codeclimate 0.0.0 → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,9 @@
1
+ module CC
2
+ module Analyzer
3
+ class NullConfig < Config
4
+ def initialize(*)
5
+ @config = { "engines" => {} }
6
+ end
7
+ end
8
+ end
9
+ 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
@@ -0,0 +1,14 @@
1
+ module CC
2
+ module Analyzer
3
+ class UnitName
4
+ attr_reader :full_name
5
+ attr_reader :local_name
6
+
7
+ def initialize(full_name, local_name)
8
+ @full_name = full_name
9
+ @local_name = local_name
10
+ end
11
+
12
+ end
13
+ end
14
+ 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