codeclimate 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/codeclimate +8 -0
- data/config/engines.yml +95 -0
- data/lib/cc/analyzer.rb +26 -0
- data/lib/cc/analyzer/config.rb +73 -0
- data/lib/cc/analyzer/engine.rb +108 -0
- data/lib/cc/analyzer/engine_registry.rb +21 -0
- data/lib/cc/analyzer/filesystem.rb +48 -0
- data/lib/cc/analyzer/formatters.rb +19 -0
- data/lib/cc/analyzer/formatters/formatter.rb +29 -0
- data/lib/cc/analyzer/formatters/json_formatter.rb +28 -0
- data/lib/cc/analyzer/formatters/plain_text_formatter.rb +114 -0
- data/lib/cc/analyzer/formatters/spinner.rb +34 -0
- data/lib/cc/analyzer/issue_sorter.rb +30 -0
- data/lib/cc/analyzer/location_description.rb +51 -0
- data/lib/cc/analyzer/null_config.rb +9 -0
- data/lib/cc/analyzer/source_buffer.rb +49 -0
- data/lib/cc/analyzer/unit_name.rb +14 -0
- data/lib/cc/cli.rb +17 -0
- data/lib/cc/cli/analyze.rb +91 -0
- data/lib/cc/cli/command.rb +65 -0
- data/lib/cc/cli/console.rb +12 -0
- data/lib/cc/cli/engines.rb +14 -0
- data/lib/cc/cli/engines/disable.rb +31 -0
- data/lib/cc/cli/engines/enable.rb +35 -0
- data/lib/cc/cli/engines/engine_command.rb +47 -0
- data/lib/cc/cli/engines/install.rb +49 -0
- data/lib/cc/cli/engines/list.rb +14 -0
- data/lib/cc/cli/engines/remove.rb +29 -0
- data/lib/cc/cli/help.rb +36 -0
- data/lib/cc/cli/init.rb +53 -0
- data/lib/cc/cli/runner.rb +60 -0
- data/lib/cc/cli/validate_config.rb +75 -0
- data/lib/cc/cli/version.rb +15 -0
- metadata +217 -10
- data/lib/codeclimate.rb +0 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
module CC
|
2
|
+
module Analyzer
|
3
|
+
module Formatters
|
4
|
+
class JSONFormatter < Formatter
|
5
|
+
|
6
|
+
def engine_running(engine)
|
7
|
+
@active_engine = engine
|
8
|
+
yield
|
9
|
+
@active_engine = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def write(data)
|
13
|
+
return unless data.present?
|
14
|
+
|
15
|
+
document = JSON.parse(data)
|
16
|
+
document["engine_name"] = @active_engine.name
|
17
|
+
puts document.to_json
|
18
|
+
end
|
19
|
+
|
20
|
+
def failed(output)
|
21
|
+
$stderr.puts "\nAnalysis failed with the following output:"
|
22
|
+
$stderr.puts output
|
23
|
+
exit 1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require "rainbow"
|
2
|
+
require "tty/spinner"
|
3
|
+
require "active_support/number_helper"
|
4
|
+
|
5
|
+
module CC
|
6
|
+
module Analyzer
|
7
|
+
module Formatters
|
8
|
+
class PlainTextFormatter < Formatter
|
9
|
+
|
10
|
+
def started
|
11
|
+
puts colorize("Starting analysis", :green)
|
12
|
+
end
|
13
|
+
|
14
|
+
def write(data)
|
15
|
+
if data.present?
|
16
|
+
json = JSON.parse(data)
|
17
|
+
if @active_engine
|
18
|
+
json["engine_name"] = @active_engine.name
|
19
|
+
end
|
20
|
+
|
21
|
+
case json["type"].downcase
|
22
|
+
when "issue"
|
23
|
+
issues << json
|
24
|
+
when "warning"
|
25
|
+
warnings << json
|
26
|
+
else
|
27
|
+
raise "Invalid type found: #{json["type"]}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def finished
|
33
|
+
puts
|
34
|
+
|
35
|
+
issues_by_path.each do |path, file_issues|
|
36
|
+
puts colorize("== #{path} (#{pluralize(file_issues.size, "issue")}) ==", :yellow)
|
37
|
+
|
38
|
+
IssueSorter.new(file_issues).by_location.each do |issue|
|
39
|
+
if location = issue["location"]
|
40
|
+
print(colorize(LocationDescription.new(location, ": "), :cyan))
|
41
|
+
end
|
42
|
+
|
43
|
+
print(issue["description"])
|
44
|
+
print(colorize(" [#{issue["engine_name"]}]", "#333333"))
|
45
|
+
puts
|
46
|
+
end
|
47
|
+
puts
|
48
|
+
end
|
49
|
+
|
50
|
+
print(colorize("Analysis complete! Found #{pluralize(issues.size, "issue")}", :green))
|
51
|
+
if warnings.size > 0
|
52
|
+
print(colorize(" and #{pluralize(warnings.size, "warning")}", :green))
|
53
|
+
end
|
54
|
+
puts(colorize(".", :green))
|
55
|
+
end
|
56
|
+
|
57
|
+
def engine_running(engine)
|
58
|
+
@active_engine = engine
|
59
|
+
with_spinner("Running #{engine.name}: ") do
|
60
|
+
yield
|
61
|
+
end
|
62
|
+
@active_engine = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def failed(output)
|
66
|
+
spinner.stop("Failed")
|
67
|
+
puts colorize("\nAnalysis failed with the following output:", :red)
|
68
|
+
puts output
|
69
|
+
exit 1
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def spinner(text = nil)
|
75
|
+
@spinner ||= Spinner.new(text)
|
76
|
+
end
|
77
|
+
|
78
|
+
def with_spinner(text)
|
79
|
+
spinner(text).start
|
80
|
+
yield
|
81
|
+
ensure
|
82
|
+
spinner.stop
|
83
|
+
@spinner = nil
|
84
|
+
end
|
85
|
+
|
86
|
+
def colorize(string, *args)
|
87
|
+
rainbow.wrap(string).color(*args)
|
88
|
+
end
|
89
|
+
|
90
|
+
def rainbow
|
91
|
+
@rainbow ||= Rainbow.new.tap do |rainbow|
|
92
|
+
rainbow.enabled = false unless @output.tty?
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def issues
|
97
|
+
@issues ||= []
|
98
|
+
end
|
99
|
+
|
100
|
+
def issues_by_path
|
101
|
+
issues.group_by { |i| i['location']['path'] }.sort
|
102
|
+
end
|
103
|
+
|
104
|
+
def warnings
|
105
|
+
@warnings ||= []
|
106
|
+
end
|
107
|
+
|
108
|
+
def pluralize(number, noun)
|
109
|
+
"#{ActiveSupport::NumberHelper.number_to_delimited(number)} #{noun.pluralize(number)}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module CC
|
2
|
+
module Analyzer
|
3
|
+
module Formatters
|
4
|
+
class Spinner
|
5
|
+
def initialize(text)
|
6
|
+
@spinner = TTY::Spinner.new(text)
|
7
|
+
end
|
8
|
+
|
9
|
+
def start
|
10
|
+
@thread = Thread.new do
|
11
|
+
loop do
|
12
|
+
@spinning = true
|
13
|
+
spinner.spin
|
14
|
+
sleep 0.075
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def stop(text = "Done!")
|
20
|
+
if @spinning
|
21
|
+
spinner.stop(text)
|
22
|
+
print("\n")
|
23
|
+
@thread.kill
|
24
|
+
end
|
25
|
+
@spinning = false
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :spinner
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module CC
|
2
|
+
module Analyzer
|
3
|
+
class IssueSorter
|
4
|
+
def initialize(issues)
|
5
|
+
@issues = issues
|
6
|
+
end
|
7
|
+
|
8
|
+
def by_location
|
9
|
+
@issues.sort_by { |i| line_or_offset(i) }
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def line_or_offset(issue)
|
15
|
+
location = issue["location"]
|
16
|
+
|
17
|
+
case
|
18
|
+
when location["lines"]
|
19
|
+
location["lines"]["begin"].to_i
|
20
|
+
when location["positions"] && location["positions"]["begin"]["line"]
|
21
|
+
location["positions"]["begin"]["line"].to_i + location["positions"]["begin"]["column"].to_i
|
22
|
+
when location["positions"] && location["positions"]["begin"]["offset"]
|
23
|
+
location["positions"]["begin"]["offset"].to_i + 1_000_000_000 # push offsets to end of list
|
24
|
+
else
|
25
|
+
0 # whole-file issues are first
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module CC
|
2
|
+
module Analyzer
|
3
|
+
class LocationDescription
|
4
|
+
def initialize(location, suffix = "")
|
5
|
+
@location = location
|
6
|
+
@suffix = suffix
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
str = ""
|
11
|
+
if location["lines"]
|
12
|
+
str << render_lines
|
13
|
+
elsif positions = location["positions"]
|
14
|
+
str << render_position(positions["begin"])
|
15
|
+
|
16
|
+
if positions["end"]
|
17
|
+
str << "-"
|
18
|
+
str << render_position(positions["end"])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
str << suffix unless str.blank?
|
23
|
+
|
24
|
+
str
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :location, :suffix
|
30
|
+
|
31
|
+
def render_lines
|
32
|
+
str = location["lines"]["begin"].to_s
|
33
|
+
str << "-#{location["lines"]["end"]}" if location["lines"]["end"]
|
34
|
+
str
|
35
|
+
end
|
36
|
+
|
37
|
+
def render_position(position)
|
38
|
+
str = ""
|
39
|
+
|
40
|
+
if position["line"]
|
41
|
+
str << position["line"].to_s
|
42
|
+
str << ":#{position["column"]}" if position["column"]
|
43
|
+
elsif position["offset"]
|
44
|
+
str << position["offset"].to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
str
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# Adapted from https://github.com/whitequark/parser/blob/master/lib/parser/source/buffer.rb
|
2
|
+
module CC
|
3
|
+
module Analyzer
|
4
|
+
class SourceBuffer
|
5
|
+
attr_reader :name
|
6
|
+
attr_reader :source
|
7
|
+
|
8
|
+
def initialize(name, source)
|
9
|
+
@name = name
|
10
|
+
@source = source
|
11
|
+
end
|
12
|
+
|
13
|
+
def decompose_position(position)
|
14
|
+
line_no, line_begin = line_for(position)
|
15
|
+
|
16
|
+
[ 1 + line_no, position - line_begin ]
|
17
|
+
end
|
18
|
+
|
19
|
+
def line_count
|
20
|
+
@source.lines.count
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def line_for(position)
|
26
|
+
line_begins.bsearch do |line, line_begin|
|
27
|
+
line_begin <= position
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def line_begins
|
32
|
+
unless @line_begins
|
33
|
+
@line_begins, index = [ [ 0, 0 ] ], 1
|
34
|
+
|
35
|
+
@source.each_char do |char|
|
36
|
+
if char == "\n"
|
37
|
+
@line_begins.unshift [ @line_begins.length, index ]
|
38
|
+
end
|
39
|
+
|
40
|
+
index += 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
@line_begins
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/cc/cli.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "active_support"
|
2
|
+
require "active_support/core_ext"
|
3
|
+
require "cc/analyzer"
|
4
|
+
|
5
|
+
module CC
|
6
|
+
module CLI
|
7
|
+
autoload :Analyze, "cc/cli/analyze"
|
8
|
+
autoload :Command, "cc/cli/command"
|
9
|
+
autoload :Console, "cc/cli/console"
|
10
|
+
autoload :Engines, "cc/cli/engines"
|
11
|
+
autoload :Help, "cc/cli/help"
|
12
|
+
autoload :Init, "cc/cli/init"
|
13
|
+
autoload :Runner, "cc/cli/runner"
|
14
|
+
autoload :ValidateConfig, "cc/cli/validate_config"
|
15
|
+
autoload :Version, "cc/cli/version"
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
|
3
|
+
module CC
|
4
|
+
module CLI
|
5
|
+
class Analyze < Command
|
6
|
+
include CC::Analyzer
|
7
|
+
|
8
|
+
def initialize(args = [])
|
9
|
+
super
|
10
|
+
|
11
|
+
process_args
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
require_codeclimate_yml
|
16
|
+
if engines.empty?
|
17
|
+
fatal("No engines enabled. Add some to your .codeclimate.yml file!")
|
18
|
+
end
|
19
|
+
|
20
|
+
formatter.started
|
21
|
+
|
22
|
+
engines.each do |engine|
|
23
|
+
formatter.engine_running(engine) do
|
24
|
+
engine.run(formatter)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
formatter.finished
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def process_args
|
34
|
+
case @args.first
|
35
|
+
when '-f'
|
36
|
+
@args.shift # throw out the -f
|
37
|
+
@formatter = Formatters.resolve(@args.shift)
|
38
|
+
end
|
39
|
+
rescue Formatters::Formatter::InvalidFormatterError => e
|
40
|
+
fatal(e.message)
|
41
|
+
end
|
42
|
+
|
43
|
+
def config
|
44
|
+
@config ||= if filesystem.exist?(CODECLIMATE_YAML)
|
45
|
+
config_body = filesystem.read_path(CODECLIMATE_YAML)
|
46
|
+
config = Config.new(config_body)
|
47
|
+
else
|
48
|
+
config = NullConfig.new
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def engine_registry
|
53
|
+
@engine_registry ||= EngineRegistry.new
|
54
|
+
end
|
55
|
+
|
56
|
+
def engine_config(engine_name)
|
57
|
+
config.engine_config(engine_name).
|
58
|
+
merge!(exclude_paths: exclude_paths).to_json
|
59
|
+
end
|
60
|
+
|
61
|
+
def exclude_paths
|
62
|
+
if config.exclude_paths
|
63
|
+
filesystem.files_matching(config.exclude_paths)
|
64
|
+
else
|
65
|
+
[]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def engines
|
70
|
+
@engines ||= config.engine_names.map do |engine_name|
|
71
|
+
Engine.new(
|
72
|
+
engine_name,
|
73
|
+
engine_registry[engine_name],
|
74
|
+
path,
|
75
|
+
engine_config(engine_name),
|
76
|
+
SecureRandom.uuid
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def formatter
|
82
|
+
@formatter ||= Formatters::PlainTextFormatter.new
|
83
|
+
end
|
84
|
+
|
85
|
+
def path
|
86
|
+
ENV['CODE_PATH']
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|