diff --git a/lib/cc/engine/analyzers/kotlin/main.rb b/lib/cc/engine/analyzers/kotlin/main.rb new file mode 100644 index 00000000..76a6b4a3 --- /dev/null +++ b/lib/cc/engine/analyzers/kotlin/main.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "flay" +require "json" +require "cc/engine/analyzers/reporter" +require "cc/engine/analyzers/analyzer_base" + +module CC + module Engine + module Analyzers + module Kotlin + class Main < CC::Engine::Analyzers::Base + LANGUAGE = "kotlin".freeze + PATTERNS = ["**/*.kt"].freeze + DEFAULT_MASS_THRESHOLD = 40 + DEFAULT_FILTERS = [ + "(IMPORT_LIST ___)".freeze, + "(PACKAGE_DIRECTIVE ___)".freeze, + "(KDoc ___)".freeze, + "(EOL_COMMENT ___)".freeze, + ].freeze + POINTS_PER_OVERAGE = 10_000 + REQUEST_PATH = "/kotlin".freeze + + def use_sexp_lines? + false + end + + private + + def process_file(file) + parse(file, REQUEST_PATH) + end + + def default_filters + DEFAULT_FILTERS.map { |filter| Sexp::Matcher.parse filter } + end + end + end + end + end +end diff --git a/lib/cc/engine/duplication.rb b/lib/cc/engine/duplication.rb index 43120ec5..76ef5161 100644 --- a/lib/cc/engine/duplication.rb +++ b/lib/cc/engine/duplication.rb @@ -4,6 +4,7 @@ require "cc/engine/parse_metrics" require "cc/engine/analyzers/ruby/main" require "cc/engine/analyzers/java/main" +require "cc/engine/analyzers/kotlin/main" require "cc/engine/analyzers/javascript/main" require "cc/engine/analyzers/go/main" require "cc/engine/analyzers/php/main" @@ -23,6 +24,7 @@ class Duplication "ruby" => ::CC::Engine::Analyzers::Ruby::Main, "java" => ::CC::Engine::Analyzers::Java::Main, "javascript" => ::CC::Engine::Analyzers::Javascript::Main, + "kotlin" => ::CC::Engine::Analyzers::Kotlin::Main, "php" => ::CC::Engine::Analyzers::Php::Main, "python" => ::CC::Engine::Analyzers::Python::Main, "typescript" => ::CC::Engine::Analyzers::TypeScript::Main, diff --git a/spec/cc/engine/analyzers/engine_config_spec.rb b/spec/cc/engine/analyzers/engine_config_spec.rb index fd5c959d..0a7ec6c9 100644 --- a/spec/cc/engine/analyzers/engine_config_spec.rb +++ b/spec/cc/engine/analyzers/engine_config_spec.rb @@ -46,6 +46,7 @@ "ruby" => {}, "java" => {}, "javascript" => {}, + "kotlin" => {}, "php" => {}, "python" => {}, "typescript" => {}, diff --git a/spec/cc/engine/analyzers/kotlin/kotlin_spec.rb b/spec/cc/engine/analyzers/kotlin/kotlin_spec.rb new file mode 100644 index 00000000..136d9732 --- /dev/null +++ b/spec/cc/engine/analyzers/kotlin/kotlin_spec.rb @@ -0,0 +1,246 @@ +require "spec_helper" +require "cc/engine/analyzers/kotlin/main" +require "cc/engine/analyzers/engine_config" + +module CC::Engine::Analyzers + RSpec.describe Kotlin::Main, in_tmpdir: true do + include AnalyzerSpecHelpers + + describe "#run" do + let(:engine_conf) { EngineConfig.new({}) } + + it "prints an issue for similar code" do + create_source_file("foo.kt", <<-EOF) + class ArrayDemo { + fun foo() { + val anArray: Array = Array(10) + + for (i in 0..10) { + anArray[i] = i + } + + for (i in 0..10) { + println(anArray[i]) + } + + println("") + } + + fun bar() { + val anArray: Array = Array(10) + + for (i in 0..10) { + anArray[i] = i + } + + for (i in 0..10) { + println(anArray[i]) + } + + println("") + } + } + EOF + + issues = run_engine(engine_conf).strip.split("\0") + result = issues.first.strip + json = JSON.parse(result) + + expect(json["type"]).to eq("issue") + expect(json["check_name"]).to eq("similar-code") + expect(json["description"]).to eq("Similar blocks of code found in 2 locations. Consider refactoring.") + expect(json["categories"]).to eq(["Duplication"]) + expect(json["location"]).to eq({ + "path" => "foo.kt", + "lines" => { "begin" => 2, "end" => 14 }, + }) + expect(json["other_locations"]).to eq([ + {"path" => "foo.kt", "lines" => { "begin" => 16, "end" => 28 } }, + ]) + expect(json["severity"]).to eq(CC::Engine::Analyzers::Base::MAJOR) + end + + it "prints an issue for identical code" do + create_source_file("foo.kt", <<-EOF) + class ArrayDemo { + fun foo(anArray: Array) { + for (i in anArray.indices) { + println(anArray[i] + " ") + } + + println("") + } + + fun foo(anArray: Array) { + for (i in anArray.indices) { + println(anArray[i] + " ") + } + + println("") + } + } + EOF + + issues = run_engine(engine_conf).strip.split("\0") + result = issues.first.strip + json = JSON.parse(result) + + expect(json["type"]).to eq("issue") + expect(json["check_name"]).to eq("identical-code") + expect(json["description"]).to eq("Identical blocks of code found in 2 locations. Consider refactoring.") + expect(json["categories"]).to eq(["Duplication"]) + expect(json["location"]).to eq({ + "path" => "foo.kt", + "lines" => { "begin" => 2, "end" => 8 }, + }) + expect(json["other_locations"]).to eq([ + {"path" => "foo.kt", "lines" => { "begin" => 10, "end" => 16 } }, + ]) + expect(json["severity"]).to eq(CC::Engine::Analyzers::Base::MAJOR) + end + + it "outputs a warning for unprocessable errors" do + create_source_file("foo.kt", <<-EOF) + --- + EOF + + expect(CC.logger).to receive(:warn).with(/Response status: 422/) + expect(CC.logger).to receive(:warn).with(/Skipping/) + run_engine(engine_conf) + end + + it "ignores import and package declarations" do + create_source_file("foo.kt", <<-EOF) + package org.springframework.rules.constraint; + + import java.util.Comparator; + + import org.springframework.rules.constraint.Constraint; + import org.springframework.rules.closure.BinaryConstraint; + EOF + + create_source_file("bar.kt", <<-EOF) + package org.springframework.rules.constraint; + + import java.util.Comparator; + + import org.springframework.rules.constraint.Constraint; + import org.springframework.rules.closure.BinaryConstraint; + EOF + + issues = run_engine(engine_conf).strip.split("\0") + expect(issues).to be_empty + end + + it "prints an issue for similar code when the only difference is the value of a literal" do + create_source_file("foo.kt", <<-EOF) + class ArrayDemo { + fun foo() { + val scott = arrayOfInt( + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F + ) + + val anArray: Array = Array(10) + + for (i in 0..10) { + anArray[i] = i + } + + for (i in 0..10) { + println(anArray[i] + " ") + } + + println() + } + + fun foo() { + val scott = arrayOfInt( + 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7 + ) + + val anArray: Array = Array(10) + + for (i in 0..10) { + anArray[i] = i + } + + for (i in 0..10) { + println(anArray[i] + " ") + } + + println() + } + } + EOF + + issues = run_engine(engine_conf).strip.split("\0") + expect(issues.length).to be > 0 + result = issues.first.strip + json = JSON.parse(result) + + expect(json["type"]).to eq("issue") + expect(json["check_name"]).to eq("similar-code") + + expect(json["description"]).to eq("Similar blocks of code found in 2 locations. Consider refactoring.") + expect(json["categories"]).to eq(["Duplication"]) + expect(json["location"]).to eq({ + "path" => "foo.kt", + "lines" => { "begin" => 2, "end" => 18 }, + }) + expect(json["other_locations"]).to eq([ + {"path" => "foo.kt", "lines" => { "begin" => 20, "end" => 36 } }, + ]) + expect(json["severity"]).to eq(CC::Engine::Analyzers::Base::MAJOR) + end + + it "ignores comment docs and comments" do + create_source_file("foo.kt", <<-EOF) + /******************************************************************** + * Copyright (C) 2017 by Max Lv + *******************************************************************/ + + package com.github.shadowsocks.acl + // Comment here + + import org.junit.Assert + import org.junit.Test + + class AclTest { + // Comment here + companion object { + private const val INPUT1 = """[proxy_all] + [bypass_list] + 1.0.1.0/24 + (^|\.)4tern\.com${'$'} + """ + } + + @Test + fun parse() { + Assert.assertEquals(INPUT1, Acl().fromReader(INPUT1.reader()).toString()); + } + } + EOF + + create_source_file("bar.kt", <<-EOF) + /********************************************************************* + * Copyright (C) 2017 by Max Lv + ********************************************************************/ + + package com.evernote.android.job + // Comment here + + object JobConstants { + // Comment here + const val DATABASE_NAME = JobStorage.DATABASE_NAME + const val PREF_FILE_NAME = JobStorage.PREF_FILE_NAME + } + EOF + + issues = run_engine(engine_conf).strip.split("\0") + expect(issues).to be_empty + end + + end + end +end