Skip to content
This repository was archived by the owner on Jan 19, 2025. It is now read-only.

Commit b023dc7

Browse files
lars-reimannMasara
andauthored
feat: todo annotation (#537)
* feat: process description annotations in backend * feat: update validation in backend * feat(backend): actually trigger processing * style: apply automatic fixes of linters * feat(backend): process todo annotations * feat(backend): update validation * feat(backend): code generation for todo comments * style: apply automatic fixes of linters * feat: Adding description annotation (WIP) * revert: changes to Kotlin files * feat: Added description annotation, a filter for it and adjusted the heatmap to also count description annotations. * feat: Added todo annotation, a filter for it and adjusted the heatmap to also count todo annotations. * fix: issues after merge * fix: build errors * fix: server crash * style: apply automatic fixes of linters Co-authored-by: lars-reimann <[email protected]> Co-authored-by: Arsam Islami <[email protected]>
1 parent 9f3ff94 commit b023dc7

File tree

27 files changed

+400
-22
lines changed

27 files changed

+400
-22
lines changed

api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,14 @@ fun PythonAttribute.toPythonCode() = buildString {
117117
}
118118

119119
fun PythonClass.toPythonCode() = buildString {
120+
val todoComment = todoComment(todo)
120121
val docstring = docstring()
121122
val constructorString = constructor?.toPythonCode() ?: ""
122123
val methodsString = methods.joinToString("\n\n") { it.toPythonCode() }
123124

125+
if (todoComment.isNotBlank()) {
126+
appendLine(todoComment)
127+
}
124128
appendLine("class $name:")
125129
if (docstring.isNotBlank()) {
126130
appendIndented("\"\"\"\n")
@@ -143,6 +147,7 @@ fun PythonClass.toPythonCode() = buildString {
143147
}
144148

145149
fun PythonConstructor.toPythonCode() = buildString {
150+
val todoComment = todoComment(todo)
146151
val parametersString = parameters.toPythonCode()
147152
val boundariesString = parameters
148153
.mapNotNull { it.boundary?.toPythonCode(it.name) }
@@ -155,6 +160,9 @@ fun PythonConstructor.toPythonCode() = buildString {
155160
?.let { "self.instance = ${it.toPythonCode()}" }
156161
?: ""
157162

163+
if (todoComment.isNotBlank()) {
164+
appendLine(todoComment)
165+
}
158166
appendLine("def __init__($parametersString):")
159167
if (boundariesString.isNotBlank()) {
160168
appendIndented(boundariesString)
@@ -202,6 +210,7 @@ fun PythonEnumInstance.toPythonCode(enumName: String): String {
202210
}
203211

204212
fun PythonFunction.toPythonCode() = buildString {
213+
val todoComment = todoComment(todo)
205214
val parametersString = parameters.toPythonCode()
206215
val docstring = docstring()
207216
val boundariesString = parameters
@@ -211,6 +220,9 @@ fun PythonFunction.toPythonCode() = buildString {
211220
?.let { "return ${it.toPythonCode()}" }
212221
?: ""
213222

223+
if (todoComment.isNotBlank()) {
224+
appendLine(todoComment)
225+
}
214226
if (isStaticMethod()) {
215227
appendLine("@staticmethod")
216228
}
@@ -357,3 +369,18 @@ fun Boundary.toPythonCode(parameterName: String) = buildString {
357369
appendIndented("raise ValueError(f'Valid values of $parameterName must be less than or equal to $upperIntervalLimit, but {$parameterName} was assigned.')")
358370
}
359371
}
372+
373+
fun todoComment(message: String) = buildString {
374+
if (message.isBlank()) {
375+
return ""
376+
}
377+
378+
val lines = message.lines()
379+
val firstLine = lines.first()
380+
val remainingLines = lines.drop(1)
381+
382+
appendLine("# TODO: $firstLine")
383+
remainingLines.forEach {
384+
appendIndented(it, indent = " ".repeat(8))
385+
}
386+
}

api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/codegen/Util.kt

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,7 @@ internal fun String.prependIndentUnlessBlank(indent: String = " "): String {
1111
.joinToString("\n")
1212
}
1313

14-
internal fun StringBuilder.appendIndented(init: StringBuilder.() -> Unit): StringBuilder {
15-
val stringToIndent = StringBuilder().apply(init).toString()
16-
append(stringToIndent.prependIndentUnlessBlank())
17-
return this
18-
}
19-
20-
internal fun StringBuilder.appendIndented(value: String): StringBuilder {
21-
append(value.prependIndentUnlessBlank())
14+
internal fun StringBuilder.appendIndented(value: String, indent: String = " "): StringBuilder {
15+
append(value.prependIndentUnlessBlank(indent))
2216
return this
2317
}

api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ object RequiredAnnotation : EditorAnnotation() {
128128
override val validTargets = PARAMETERS
129129
}
130130

131+
@Serializable
132+
data class TodoAnnotation(val message: String) : EditorAnnotation() {
133+
@Transient
134+
override val validTargets = ANY_DECLARATION
135+
}
136+
131137
@Serializable
132138
sealed class DefaultValue
133139

@@ -179,7 +185,6 @@ val ANY_DECLARATION = setOf(
179185
FUNCTION_PARAMETER
180186
)
181187
val GLOBAL_DECLARATIONS = setOf(CLASS, GLOBAL_FUNCTION)
182-
val CLASSES = setOf(CLASS)
183188
val FUNCTIONS = setOf(GLOBAL_FUNCTION, METHOD)
184189
val PARAMETERS = setOf(
185190
CONSTRUCTOR_PARAMETER,

api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class PythonClass(
6262
methods: List<PythonFunction> = emptyList(),
6363
var isPublic: Boolean = true,
6464
var description: String = "",
65+
var todo: String = "",
6566
override val annotations: MutableList<EditorAnnotation> = mutableListOf(),
6667
var originalClass: OriginalPythonClass? = null
6768
) : PythonDeclaration() {
@@ -81,7 +82,8 @@ class PythonClass(
8182

8283
class PythonConstructor(
8384
parameters: List<PythonParameter> = emptyList(),
84-
val callToOriginalAPI: PythonCall? = null
85+
val callToOriginalAPI: PythonCall? = null,
86+
var todo: String = ""
8587
) : PythonAstNode() {
8688

8789
val parameters = MutableContainmentList(parameters)
@@ -126,6 +128,7 @@ class PythonFunction(
126128
results: List<PythonResult> = emptyList(),
127129
var isPublic: Boolean = true,
128130
var description: String = "",
131+
var todo: String = "",
129132
var isPure: Boolean = false,
130133
override val annotations: MutableList<EditorAnnotation> = mutableListOf(),
131134
var callToOriginalAPI: PythonCall? = null
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.larsreimann.api_editor.transformation
2+
3+
import com.larsreimann.api_editor.model.TodoAnnotation
4+
import com.larsreimann.api_editor.mutable_model.PythonClass
5+
import com.larsreimann.api_editor.mutable_model.PythonDeclaration
6+
import com.larsreimann.api_editor.mutable_model.PythonFunction
7+
import com.larsreimann.api_editor.mutable_model.PythonPackage
8+
import com.larsreimann.modeling.descendants
9+
10+
/**
11+
* Processes and removes `@todo` annotations.
12+
*/
13+
fun PythonPackage.processTodoAnnotations() {
14+
this.descendants()
15+
.filterIsInstance<PythonDeclaration>()
16+
.forEach { it.processTodoAnnotations() }
17+
}
18+
19+
private fun PythonDeclaration.processTodoAnnotations() {
20+
this.annotations
21+
.filterIsInstance<TodoAnnotation>()
22+
.forEach {
23+
when (this) {
24+
is PythonClass -> this.todo = it.message
25+
is PythonFunction -> this.todo = it.message
26+
else -> {
27+
// Do nothing
28+
}
29+
}
30+
this.annotations.remove(it)
31+
}
32+
}

api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/TransformationPlan.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ private fun PythonPackage.preprocess(newPackageName: String) {
2929
private fun PythonPackage.processAnnotations() {
3030
processRemoveAnnotations()
3131
processDescriptionAnnotations()
32+
processTodoAnnotations()
3233
processRenameAnnotations()
3334
processMoveAnnotations()
3435
processBoundaryAnnotations()

api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,8 @@ class AnnotationValidator(private val annotatedPythonPackage: SerializablePython
181181

182182
companion object {
183183
private var possibleCombinations = buildMap<String, Set<String>> {
184-
this["Attribute"] = mutableSetOf("Description", "Rename")
185-
this["Boundary"] = mutableSetOf("Description", "Group", "Optional", "Rename", "Required")
184+
this["Attribute"] = mutableSetOf("Description", "Rename", "Todo")
185+
this["Boundary"] = mutableSetOf("Description", "Group", "Optional", "Rename", "Required", "Todo")
186186
this["CalledAfter"] = mutableSetOf("CalledAfter", "Description", "Group", "Move", "Pure", "Rename")
187187
this["Constant"] = mutableSetOf()
188188
this["Description"] = mutableSetOf(
@@ -195,9 +195,10 @@ class AnnotationValidator(private val annotatedPythonPackage: SerializablePython
195195
"Optional",
196196
"Pure",
197197
"Rename",
198-
"Required"
198+
"Required",
199+
"Todo"
199200
)
200-
this["Enum"] = mutableSetOf("Description", "Group", "Rename", "Required")
201+
this["Enum"] = mutableSetOf("Description", "Group", "Rename", "Required", "Todo")
201202
this["Group"] =
202203
mutableSetOf(
203204
"Boundary",
@@ -209,10 +210,11 @@ class AnnotationValidator(private val annotatedPythonPackage: SerializablePython
209210
"Optional",
210211
"Pure",
211212
"Rename",
212-
"Required"
213+
"Required",
214+
"Todo"
213215
)
214216
this["Move"] = mutableSetOf("CalledAfter", "Description", "Group", "Pure", "Rename")
215-
this["Optional"] = mutableSetOf("Boundary", "Description", "Group", "Rename")
217+
this["Optional"] = mutableSetOf("Boundary", "Description", "Group", "Rename", "Todo")
216218
this["Pure"] = mutableSetOf("CalledAfter", "Description", "Group", "Move", "Rename")
217219
this["Remove"] = mutableSetOf()
218220
this["Rename"] = mutableSetOf(
@@ -225,9 +227,19 @@ class AnnotationValidator(private val annotatedPythonPackage: SerializablePython
225227
"Move",
226228
"Optional",
227229
"Pure",
230+
"Required", "Todo"
231+
)
232+
this["Required"] = mutableSetOf("Boundary", "Description", "Enum", "Group", "Rename", "Todo")
233+
this["Todo"] = mutableSetOf(
234+
"Attribute",
235+
"Boundary",
236+
"Description",
237+
"Enum",
238+
"Group",
239+
"Optional",
240+
"Rename",
228241
"Required"
229242
)
230-
this["Required"] = mutableSetOf("Boundary", "Description", "Enum", "Group", "Rename")
231243
}
232244
}
233245
}

api-editor/backend/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,21 @@ class PythonCodeGeneratorTest {
238238
| self.testAttribute2: int = 2
239239
""".trimMargin()
240240
}
241+
242+
@Test
243+
fun `should store todo if it is not blank`() {
244+
val testClass = PythonClass(
245+
name = "TestClass",
246+
todo = "Lorem ipsum\n\nDolor sit amet"
247+
)
248+
249+
testClass.toPythonCode() shouldBe """
250+
|# TODO: Lorem ipsum
251+
| Dolor sit amet
252+
|class TestClass:
253+
| pass
254+
""".trimMargin()
255+
}
241256
}
242257

243258
@Nested
@@ -441,6 +456,20 @@ class PythonCodeGeneratorTest {
441456
| self.instance = OriginalClass()
442457
""".trimMargin()
443458
}
459+
460+
@Test
461+
fun `should store todo if it is not blank`() {
462+
val testConstructor = PythonConstructor(
463+
todo = "Lorem ipsum\n\nDolor sit amet"
464+
)
465+
466+
testConstructor.toPythonCode() shouldBe """
467+
|# TODO: Lorem ipsum
468+
| Dolor sit amet
469+
|def __init__():
470+
| pass
471+
""".trimMargin()
472+
}
444473
}
445474

446475
@Nested
@@ -687,6 +716,21 @@ class PythonCodeGeneratorTest {
687716
| pass
688717
""".trimMargin()
689718
}
719+
720+
@Test
721+
fun `should store todo if it is not blank`() {
722+
val testFunction = PythonFunction(
723+
name = "testFunction",
724+
todo = "Lorem ipsum\n\nDolor sit amet"
725+
)
726+
727+
testFunction.toPythonCode() shouldBe """
728+
|# TODO: Lorem ipsum
729+
| Dolor sit amet
730+
|def testFunction():
731+
| pass
732+
""".trimMargin()
733+
}
690734
}
691735

692736
@Nested
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.larsreimann.api_editor.transformation
2+
3+
import com.larsreimann.api_editor.model.TodoAnnotation
4+
import com.larsreimann.api_editor.mutable_model.PythonClass
5+
import com.larsreimann.api_editor.mutable_model.PythonFunction
6+
import com.larsreimann.api_editor.mutable_model.PythonModule
7+
import com.larsreimann.api_editor.mutable_model.PythonPackage
8+
import io.kotest.matchers.collections.shouldBeEmpty
9+
import io.kotest.matchers.shouldBe
10+
import org.junit.jupiter.api.BeforeEach
11+
import org.junit.jupiter.api.Test
12+
13+
class TodoAnnotationProcessorTest {
14+
private lateinit var testClass: PythonClass
15+
private lateinit var testFunction: PythonFunction
16+
private lateinit var testPackage: PythonPackage
17+
18+
@BeforeEach
19+
fun reset() {
20+
testClass = PythonClass(
21+
name = "TestClass",
22+
annotations = mutableListOf(TodoAnnotation("Refactor class"))
23+
)
24+
testFunction = PythonFunction(
25+
name = "testFunction",
26+
annotations = mutableListOf(TodoAnnotation("Refactor function"))
27+
)
28+
testPackage = PythonPackage(
29+
distribution = "testPackage",
30+
name = "testPackage",
31+
version = "1.0.0",
32+
modules = listOf(
33+
PythonModule(
34+
name = "testModule",
35+
classes = listOf(testClass),
36+
functions = listOf(testFunction)
37+
)
38+
)
39+
)
40+
}
41+
42+
@Test
43+
fun `should process TodoAnnotation of classes`() {
44+
testPackage.processTodoAnnotations()
45+
46+
testClass.todo shouldBe "Refactor class"
47+
}
48+
49+
@Test
50+
fun `should remove TodoAnnotation of classes`() {
51+
testPackage.processTodoAnnotations()
52+
53+
testClass.annotations
54+
.filterIsInstance<TodoAnnotation>()
55+
.shouldBeEmpty()
56+
}
57+
58+
@Test
59+
fun `should process TodoAnnotation of functions`() {
60+
testPackage.processTodoAnnotations()
61+
62+
testFunction.todo shouldBe "Refactor function"
63+
}
64+
65+
@Test
66+
fun `should remove TodoAnnotation of functions`() {
67+
testPackage.processTodoAnnotations()
68+
69+
testFunction.annotations
70+
.filterIsInstance<TodoAnnotation>()
71+
.shouldBeEmpty()
72+
}
73+
}

api-editor/desktop/src/main/kotlin/com/larsreimann/python_api_editor/AnnotationDropdown.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ fun AnnotationDropdown(
2929
showRemove: Boolean = false,
3030
showRename: Boolean = false,
3131
showRequired: Boolean = false,
32+
showTodo: Boolean = false,
3233
) {
3334
var expanded by remember { mutableStateOf(false) }
3435

@@ -102,6 +103,11 @@ fun AnnotationDropdown(
102103
Text(labels.getString("AnnotationDropdown.Option.Required"))
103104
}
104105
}
106+
if (showTodo) {
107+
DropdownMenuItem(onClick = {}) {
108+
Text(labels.getString("AnnotationDropdown.Option.Todo"))
109+
}
110+
}
105111
}
106112
}
107113
}

api-editor/desktop/src/main/resources/i18n/labels.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ AnnotationDropdown.Option.Pure=@pure
3737
AnnotationDropdown.Option.Remove=@remove
3838
AnnotationDropdown.Option.Rename=@rename
3939
AnnotationDropdown.Option.Required=@required
40+
AnnotationDropdown.Option.Todo=@todo

0 commit comments

Comments
 (0)