Repository: terryyin/lizard

Issue:
Kotlin support for lizard?
Hi. Any plans for lizard to be used for kotlin?

Comments:

Patch:
diff --git a/lizard_languages/__init__.py b/lizard_languages/__init__.py
index 94b9052..785a09d 100644
--- a/lizard_languages/__init__.py
+++ b/lizard_languages/__init__.py
@@ -3,6 +3,7 @@
 from .clike import CLikeReader
 from .java import JavaReader
 from .javascript import JavaScriptReader
+from .kotlin import KotlinReader
 from .python import PythonReader
 from .objc import ObjCReader
 from .ttcn import TTCNReader
@@ -37,7 +38,8 @@ def languages():
         LuaReader,
         RustReader,
         TypeScriptReader,
-        FortranReader
+        FortranReader,
+        KotlinReader
     ]
 
 
diff --git a/lizard_languages/kotlin.py b/lizard_languages/kotlin.py
new file mode 100644
index 0000000..f5c7b1b
--- /dev/null
+++ b/lizard_languages/kotlin.py
@@ -0,0 +1,92 @@
+'''
+Language parser for Apple Swift
+'''
+
+from .clike import CCppCommentsMixin
+from .code_reader import CodeReader, CodeStateMachine
+from .golike import GoLikeStates
+from .swift import SwiftReplaceLabel
+
+
+class KotlinReader(CodeReader, CCppCommentsMixin, SwiftReplaceLabel):
+    # pylint: disable=R0903
+
+    ext = ['kt', 'kts']
+    language_names = ['kotlin']
+    _conditions = {
+        'if', 'for', 'while', 'catch', '&&', '||', '?:'
+    }
+
+    def __init__(self, context):
+        super(KotlinReader, self).__init__(context)
+        self.parallel_states = [KotlinStates(context)]
+
+    @staticmethod
+    def generate_tokens(source_code, addition='', token_class=None):
+        return CodeReader.generate_tokens(
+            source_code,
+            r"|`\w+`" +
+            r"|\w+\?" +
+            r"|\w+\!!" +
+            r"|\?\?" +
+            r"|\?:" +
+            addition
+        )
+
+
+class KotlinStates(GoLikeStates):  # pylint: disable=R0903
+
+    FUNC_KEYWORD = 'fun'
+
+    def __init__(self, context, in_when_cases=False):
+        super().__init__(context)
+        self._in_when_cases = in_when_cases
+
+    def _state_global(self, token):
+        if token in ('get', 'set'):
+            self.context.push_new_function(token)
+            self._state = self._expect_function_impl
+        elif token == '->':
+            if self._in_when_cases:
+                self.context.add_condition()
+            else:
+                self.context.push_new_function("(anonymous)")
+                self._state = super(KotlinStates, self)._expect_function_impl
+        elif token in ('val', 'var', ','):
+            self._state = self._expect_declaration_name
+        elif token == 'interface':
+            self._state = self._interface
+        elif token == 'when':
+            self._state = self._when_cases
+        else:
+            super(KotlinStates, self)._state_global(token)
+
+    def _expect_declaration_name(self, token):
+        self._state = self._state_global
+
+    def _expect_function_impl(self, token):
+        if token == '{' or token == '=':
+            self.next(self._function_impl, token)
+
+    @CodeStateMachine.read_inside_brackets_then("{}")
+    def _interface(self, end_token):
+        if end_token == "}":
+            self._state = self._state_global
+
+    def _function_name(self, token):
+        if token == "<":
+            self.next(self._template, token)
+        else:
+            return super(KotlinStates, self)._function_name(token)
+
+    @CodeStateMachine.read_inside_brackets_then("<>", "_function_name")
+    def _template(self, tokens):
+        pass
+
+    def _when_cases(self, token):
+        def callback():
+            self.context.add_condition(inc=-1)
+            self.next(self._state_global)
+        if token != '{':
+            return
+        self.sub_state(KotlinStates(self.context, in_when_cases=True), callback)
diff --git a/lizard_languages/swift.py b/lizard_languages/swift.py
index dc4eed2..3b1cbcc 100644
--- a/lizard_languages/swift.py
+++ b/lizard_languages/swift.py
@@ -7,7 +7,24 @@ from .clike import CCppCommentsMixin
 from .golike import GoLikeStates
 
 
-class SwiftReader(CodeReader, CCppCommentsMixin):
+class SwiftReplaceLabel:
+    def preprocess(self, tokens):
+        tokens = list(t for t in tokens if not t.isspace() or t == '\n')
+
+        def replace_label(tokens, target, replace):
+            for i in range(0, len(tokens) - len(target)):
+                if tokens[i:i + len(target)] == target:
+                    for j, repl in enumerate(replace):
+                        tokens[i + j] = repl
+            return tokens
+
+        for k in (k for k in self.conditions if k.isalpha()):
+            tokens = replace_label(tokens, ["(", k, ":"], ["(", "_" + k, ":"])
+            tokens = replace_label(tokens, [",", k, ":"], [",", "_" + k, ":"])
+        return tokens
+
+
+class SwiftReader(CodeReader, CCppCommentsMixin, SwiftReplaceLabel):
     # pylint: disable=R0903
 
     FUNC_KEYWORD = 'def'
@@ -30,20 +47,6 @@ class SwiftReader(CodeReader, CCppCommentsMixin):
             r"|\?\?" +
             addition)
 
-    def preprocess(self, tokens):
-        tokens = list(t for t in tokens if not t.isspace() or t == '\n')
-
-        def replace_label(tokens, target, replace):
-            for i in range(0, len(tokens) - len(target)):
-                if tokens[i:i + len(target)] == target:
-                    for j, repl in enumerate(replace):
-                        tokens[i + j] = repl
-            return tokens
-        for k in (k for k in self.conditions if k.isalpha()):
-            tokens = replace_label(tokens, ["(", k, ":"], ["(", "_" + k, ":"])
-            tokens = replace_label(tokens, [",", k, ":"], [",", "_" + k, ":"])
-        return tokens
-
 
 class SwiftStates(GoLikeStates):  # pylint: disable=R0903
     def _state_global(self, token):


Test patch:
diff --git a/test/test_languages/testKotlin.py b/test/test_languages/testKotlin.py
new file mode 100644
index 0000000..a9e94bd
--- /dev/null
+++ b/test/test_languages/testKotlin.py
@@ -0,0 +1,226 @@
+import unittest
+
+from lizard import analyze_file
+from lizard_languages import KotlinReader
+
+
+def get_kotlin_function_list(source_code):
+    return analyze_file.analyze_source_code(
+        "a.kt", source_code
+    ).function_list
+
+
+class Test_tokenizing_Kotlin(unittest.TestCase):
+
+    def check_tokens(self, expect, source):
+        tokens = list(KotlinReader.generate_tokens(source))
+        self.assertEqual(expect, tokens)
+
+    def test_dollar_var(self):
+        self.check_tokens(['`a`'], '`a`')
+
+
+class Test_parser_for_Kotlin(unittest.TestCase):
+
+    def test_empty(self):
+        functions = get_kotlin_function_list("")
+        self.assertEqual(0, len(functions))
+
+    def test_no_function(self):
+        result = get_kotlin_function_list('''
+            for name in names {
+                println("Hello, \\(name)!")
+            }
+                ''')
+        self.assertEqual(0, len(result))
+
+    def test_one_function(self):
+        result = get_kotlin_function_list('''
+            fun sayGoodbye() { }
+                ''')
+        self.assertEqual(1, len(result))
+        self.assertEqual("sayGoodbye", result[0].name)
+        self.assertEqual(0, result[0].parameter_count)
+        self.assertEqual(1, result[0].cyclomatic_complexity)
+
+    def test_one_with_parameter(self):
+        result = get_kotlin_function_list('''
+            fun sayGoodbye(personName: String, alreadyGreeted: Bool) { }
+                ''')
+        self.assertEqual(1, len(result))
+        self.assertEqual("sayGoodbye", result[0].name)
+        self.assertEqual(2, result[0].parameter_count)
+
+    def test_one_function_with_return_value(self):
+        result = get_kotlin_function_list('''
+            fun sayGoodbye(): String {return "bye"}
+                ''')
+        self.assertEqual(1, len(result))
+        self.assertEqual("sayGoodbye", result[0].name)
+
+    def test_one_lambda_with_return_value(self):
+        result = get_kotlin_function_list('''
+            val sayGoodbye: () -> String = {"bye"}
+                ''')
+        self.assertEqual(1, len(result))
+        self.assertEqual("(anonymous)", result[0].name)
+
+    def test_one_function_with_complexity(self):
+        result = get_kotlin_function_list('''
+            fun sayGoodbye() { if ++diceRoll == 7 { diceRoll = 1 }}
+                ''')
+        self.assertEqual(2, result[0].cyclomatic_complexity)
+
+    def test_interface(self):
+        result = get_kotlin_function_list('''
+            interface p {
+                fun f1(): String
+                fun f2()
+            }
+            fun sayGoodbye() { }
+                ''')
+        self.assertEqual(1, len(result))
+        self.assertEqual("sayGoodbye", result[0].name)
+
+    def test_interface_followed_by_a_class(self):
+        result = get_kotlin_function_list('''
+            interface p {
+                fun f1(): String
+                fun f2()
+            }
+            class c { }
+                ''')
+        self.assertEqual(0, len(result))
+
+    def test_interface_with_vars(self):
+        result = get_kotlin_function_list('''
+            interface p {
+                fun f1(): String
+                fun f2()
+                val p1: String
+                val p2: String
+                    get() = "p2"
+            }
+            class c { }
+                ''')
+        self.assertEqual(0, len(result))
+
+    def test_getter(self):
+        result = get_kotlin_function_list('''
+            class Time
+            {
+                var seconds: Double = 17.0
+                var minutes: Double
+                    get() = seconds / 60
+            }
+                ''')
+        self.assertEqual("get", result[0].name)
+
+    def test_getter_setter(self):
+        result = get_kotlin_function_list('''
+            class Time
+            {
+                var seconds: Double = 17.0
+                var minutes: Double
+                    get() = seconds / 60
+                    set(newValue) {
+                        this.seconds = (newValue * 60)
+                    }
+            }
+                ''')
+        self.assertEqual("get", result[1].name)
+        self.assertEqual("set", result[0].name)
+
+    # https://docs.kotlin.org/kotlin-book/LanguageGuide/Properties.html#ID259
+    def test_explicit_getter_setter(self):
+        result = get_kotlin_function_list('''
+            var center: Point
+                get() = {
+                    val centerX = origin.x + (size.width / 2)
+                    val centerY = origin.y + (size.height / 2)
+                    return Point(x: centerX, y: centerY)
+                }
+                set(newCenter) {
+                    origin.x = newCenter.x - (size.width / 2)
+                    origin.y = newCenter.y - (size.height / 2)
+                }
+            }
+                ''')
+        self.assertEqual("set", result[0].name)
+        self.assertEqual("get", result[1].name)
+
+    def test_when_cases(self):
+        result = get_kotlin_function_list('''
+                fun cases(x: Int) {
+                    when (x) {
+                        0, 1 -> print("x == 0 or x == 1")
+                        else -> print("otherwise")
+                    }
+                }
+                ''')
+        self.assertEqual("cases", result[0].name)
+        self.assertEqual(2, result[0].cyclomatic_complexity)
+
+    def test_keyword_declarations(self):
+        result = get_kotlin_function_list('''
+            enum class Func {
+                static var `class`: Bool? = false
+                static val `interface` = 0
+                fun `get`() {}
+            }
+                ''')
+        self.assertEqual("`get`", result[0].name)
+
+    def test_generic_function(self):
+        result = get_kotlin_function_list('''
+            fun <T> f() {}
+                ''')
+        self.assertEqual("f", result[0].name)
+
+    def test_complex_generic_function(self):
+        result = get_kotlin_function_list('''
+        fun <C1, C2> f (c1: C1, c: C2): Boolean where C2 : Container {return C2.isEmpty()}
+                ''')
+        self.assertEqual("f", result[0].name)
+        self.assertEqual(2, result[0].parameter_count)
+
+    def test_elvis_operator(self):
+        result = get_kotlin_function_list(''' fun f() {
+                val keep = filteredList?.contains(ingredient) ?: true
+            }
+        ''')
+        self.assertEqual("f", result[0].name)
+        self.assertEqual(2, result[0].cyclomatic_complexity)
+
+    def test_for_label(self):
+        result = get_kotlin_function_list('''
+            fun f0() { something(for: .something) }
+            fun f1() { something(for :.something) }
+            fun f2() { something(for : .something) }
+            fun f3() { something(for: if (isValid) true else false) }
+            fun f4() { something(label1: .something, label2: .something, for: .something) }
+        ''')
+        self.assertEqual(1, result[0].cyclomatic_complexity)
+        self.assertEqual(1, result[1].cyclomatic_complexity)
+        self.assertEqual(1, result[2].cyclomatic_complexity)
+        self.assertEqual(2, result[3].cyclomatic_complexity)
+        self.assertEqual(1, result[4].cyclomatic_complexity)
+
+    def test_nested(self):
+        result = get_kotlin_function_list('''
+        fun bar() : Int {
+            fun a() : Int {
+                // Do a load of stuff
+                return 1
+            }
+            fun b() : Int {
+                // Do a load of stuff
+                return 1
+            }
+            return a() + b()
+        }
+        ''')
+        self.assertEqual(3, len(result))
+        self.assertEqual("a", result[0].name)
+        self.assertEqual("b", result[1].name)
+        self.assertEqual("bar", result[2].name)