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

Commit b5b7a4f

Browse files
toddmazierskiwfleming
authored andcommitted
Add TypeScript analyzer
Similar in approach to codeclimate/codeclimate-structure#275, this is virtually identical to the `Javascript` one, but with different `PATTERNS`, `LANGUAGE`, and `REQUEST_PATH`. The specs have been modified to include some TypeScript syntax. Addresses codeclimate/app#6325.
1 parent c092735 commit b5b7a4f

File tree

4 files changed

+246
-0
lines changed

4 files changed

+246
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
require "cc/engine/analyzers/analyzer_base"
4+
5+
module CC
6+
module Engine
7+
module Analyzers
8+
module TypeScript
9+
class Main < CC::Engine::Analyzers::Base
10+
PATTERNS = [
11+
"**/*.ts",
12+
]
13+
LANGUAGE = "typescript"
14+
DEFAULT_MASS_THRESHOLD = 45
15+
DEFAULT_FILTERS = [
16+
"(ImportDeclaration ___)",
17+
"(VariableDeclarator _ (init (CallExpression (_ (Identifier require)) ___)))",
18+
]
19+
POINTS_PER_OVERAGE = 30_000
20+
REQUEST_PATH = "/typescript"
21+
22+
def use_sexp_lines?
23+
false
24+
end
25+
26+
private
27+
28+
def process_file(file)
29+
parse(file, REQUEST_PATH)
30+
end
31+
32+
def default_filters
33+
DEFAULT_FILTERS.map { |filter| Sexp::Matcher.parse filter }
34+
end
35+
end
36+
end
37+
end
38+
end
39+
end

lib/cc/engine/duplication.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
require "cc/engine/analyzers/php/main"
99
require "cc/engine/analyzers/python/main"
1010
require "cc/engine/analyzers/reporter"
11+
require "cc/engine/analyzers/typescript/main"
1112
require "cc/engine/analyzers/engine_config"
1213
require "cc/engine/analyzers/sexp"
1314
require "flay"
@@ -22,6 +23,7 @@ class Duplication
2223
"javascript" => ::CC::Engine::Analyzers::Javascript::Main,
2324
"php" => ::CC::Engine::Analyzers::Php::Main,
2425
"python" => ::CC::Engine::Analyzers::Python::Main,
26+
"typescript" => ::CC::Engine::Analyzers::TypeScript::Main,
2527
}.freeze
2628

2729
def initialize(directory:, engine_config:, io:)

spec/cc/engine/analyzers/engine_config_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"javascript" => {},
4949
"php" => {},
5050
"python" => {},
51+
"typescript" => {},
5152
})
5253
end
5354

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
require 'spec_helper'
2+
require 'cc/engine/analyzers/typescript/main'
3+
require 'cc/engine/analyzers/reporter'
4+
require 'cc/engine/analyzers/engine_config'
5+
6+
RSpec.describe CC::Engine::Analyzers::TypeScript::Main, in_tmpdir: true do
7+
include AnalyzerSpecHelpers
8+
9+
describe "#run" do
10+
it "prints an issue for identical code" do
11+
create_source_file("foo.ts", <<-EOTS)
12+
enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT" }
13+
enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT" }
14+
enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT" }
15+
EOTS
16+
17+
issues = run_engine(engine_conf).strip.split("\0")
18+
result = issues.first.strip
19+
json = JSON.parse(result)
20+
21+
expect(json["type"]).to eq("issue")
22+
expect(json["check_name"]).to eq("identical-code")
23+
expect(json["description"]).to eq("Identical blocks of code found in 3 locations. Consider refactoring.")
24+
expect(json["categories"]).to eq(["Duplication"])
25+
expect(json["location"]).to eq({
26+
"path" => "foo.ts",
27+
"lines" => { "begin" => 1, "end" => 1 },
28+
})
29+
expect(json["remediation_points"]).to eq(990_000)
30+
expect(json["other_locations"]).to eq([
31+
{"path" => "foo.ts", "lines" => { "begin" => 2, "end" => 2} },
32+
{"path" => "foo.ts", "lines" => { "begin" => 3, "end" => 3} },
33+
])
34+
expect(json["content"]["body"]).to match(/This issue has a mass of 24/)
35+
expect(json["fingerprint"]).to eq("a53b767d2f602f832540ef667ca0618f")
36+
expect(json["severity"]).to eq(CC::Engine::Analyzers::Base::MAJOR)
37+
end
38+
39+
it "prints an issue for similar code" do
40+
create_source_file("foo.ts", <<-EOTS)
41+
enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT" }
42+
enum Direction { Up = "up", Down = "down", Left = "left", Right = "right" }
43+
enum Direction { up = "UP", down = "DOWN", left = "LEFT", right = "RIGHT" }
44+
EOTS
45+
46+
issues = run_engine(engine_conf).strip.split("\0")
47+
result = issues.first.strip
48+
json = JSON.parse(result)
49+
50+
expect(json["type"]).to eq("issue")
51+
expect(json["check_name"]).to eq("similar-code")
52+
expect(json["description"]).to eq("Similar blocks of code found in 3 locations. Consider refactoring.")
53+
expect(json["categories"]).to eq(["Duplication"])
54+
expect(json["location"]).to eq({
55+
"path" => "foo.ts",
56+
"lines" => { "begin" => 1, "end" => 1 },
57+
})
58+
expect(json["remediation_points"]).to eq(990_000)
59+
expect(json["other_locations"]).to eq([
60+
{"path" => "foo.ts", "lines" => { "begin" => 2, "end" => 2} },
61+
{"path" => "foo.ts", "lines" => { "begin" => 3, "end" => 3} },
62+
])
63+
expect(json["content"]["body"]).to match(/This issue has a mass of 24/)
64+
expect(json["fingerprint"]).to eq("ede3452b637e0bc021541e6369b9362e")
65+
expect(json["severity"]).to eq(CC::Engine::Analyzers::Base::MAJOR)
66+
end
67+
68+
it "handles ES6 spread params" do
69+
create_source_file("foo.tsx", <<-EOTS)
70+
const ThingClass = React.createClass({
71+
propTypes: {
72+
...OtherThing.propTypes,
73+
otherProp: "someVal"
74+
}
75+
});
76+
EOTS
77+
78+
expect(CC.logger).not_to receive(:info).with(/Skipping file/)
79+
run_engine(engine_conf)
80+
end
81+
82+
it "skips unparsable files" do
83+
create_source_file("foo.ts", <<-EOTS)
84+
function () { do(); // missing closing brace
85+
EOTS
86+
87+
expect(CC.logger).to receive(:warn).with(/Skipping \.\/foo\.ts/)
88+
expect(CC.logger).to receive(:warn).with("Response status: 422")
89+
expect(run_engine(engine_conf)).to eq("")
90+
end
91+
92+
it "handles parser 500s" do
93+
create_source_file("foo.ts", <<-EOTS)
94+
EOTS
95+
96+
error = CC::Parser::Client::HTTPError.new(500, "Error processing file: ./foo.ts")
97+
allow(CC::Parser).to receive(:parse).with("", "/typescript").and_raise(error)
98+
99+
expect(CC.logger).to receive(:error).with("Error processing file: ./foo.ts")
100+
expect(CC.logger).to receive(:error).with(error.message)
101+
102+
expect { run_engine(engine_conf) }.to raise_error(error)
103+
end
104+
end
105+
106+
it "does not flag duplicate comments" do
107+
create_source_file("foo.ts", <<-EOTS)
108+
// A comment.
109+
// A comment.
110+
111+
/* A comment. */
112+
/* A comment. */
113+
EOTS
114+
115+
expect(run_engine(engine_conf)).to be_empty
116+
end
117+
118+
it "ignores imports" do
119+
create_source_file("foo.ts", <<~EOTS)
120+
import React, { Component, PropTypes } from 'react'
121+
import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow } from 'material-ui/Table'
122+
import values from 'lodash/values'
123+
import { v4 } from 'uuid'
124+
EOTS
125+
126+
create_source_file("bar.ts", <<~EOTS)
127+
import React, { Component, PropTypes } from 'react'
128+
import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow } from 'material-ui/Table'
129+
import values from 'lodash/values'
130+
import { v4 } from 'uuid'
131+
EOTS
132+
133+
issues = run_engine(engine_conf).strip.split("\0")
134+
expect(issues).to be_empty
135+
end
136+
137+
it "ignores requires" do
138+
create_source_file("foo.ts", <<~EOTS)
139+
const a = require('foo'),
140+
b = require('bar'),
141+
c = require('baz'),
142+
d = require('bam');
143+
EOTS
144+
145+
create_source_file("bar.ts", <<~EOTS)
146+
const a = require('foo'),
147+
b = require('bar'),
148+
c = require('baz'),
149+
d = require('bam');
150+
EOTS
151+
152+
issues = run_engine(engine_conf).strip.split("\0")
153+
expect(issues).to be_empty
154+
end
155+
156+
it "outputs the correct line numbers for ASTs missing line details (codeclimate/app#6227)" do
157+
create_source_file("foo.ts", <<~EOTS)
158+
`/movie?${getQueryString({ movie_id: movieId })}`
159+
EOTS
160+
161+
create_source_file("bar.ts", <<~EOTS)
162+
var greeting = "hello";
163+
164+
`/movie?${getQueryString({ movie_id: movieId })}`
165+
EOTS
166+
167+
issues = run_engine(engine_conf).strip.split("\0")
168+
expect(issues).to_not be_empty
169+
170+
issues.map! { |issue| JSON.parse(issue) }
171+
172+
foo_issue = issues.detect { |issue| issue.fetch("location").fetch("path") == "foo.ts" }
173+
expect(foo_issue["location"]).to eq({
174+
"path" => "foo.ts",
175+
"lines" => { "begin" => 1, "end" => 1 },
176+
})
177+
178+
bar_issue = issues.detect { |issue| issue.fetch("location").fetch("path") == "bar.ts" }
179+
expect(bar_issue["location"]).to eq({
180+
"path" => "bar.ts",
181+
"lines" => { "begin" => 3, "end" => 3 },
182+
})
183+
end
184+
185+
def engine_conf
186+
CC::Engine::Analyzers::EngineConfig.new({
187+
'config' => {
188+
'checks' => {
189+
'similar-code' => {
190+
'enabled' => true,
191+
},
192+
'identical-code' => {
193+
'enabled' => true,
194+
},
195+
},
196+
'languages' => {
197+
'typescript' => {
198+
'mass_threshold' => 1,
199+
},
200+
},
201+
},
202+
})
203+
end
204+
end

0 commit comments

Comments
 (0)