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.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6842c09f09430b3567358c4bc07a5f2915216b6b
|
4
|
+
data.tar.gz: 39475d633f642086e097cb601c76d7a4c2ff960a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 67d8f45e400b833b025baa3b0e1aa18ebf19ec920f7ab92e83cd8d0c02ee37811ab40e9f2e5307e8b6e5b81f5f3e287d695c28c9782d48645da0ab967fb554f3
|
7
|
+
data.tar.gz: 3c3492bcf903f23da01a83e0f775799c4a984c479b45a7312cb6ba75746e9fa0a180c1cd7f7ae120c9aad5d44b06f5002acfd0b70f43dcdb862b643a10d1243d
|
data/bin/codeclimate
ADDED
data/config/engines.yml
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# This file lists all the engines available to be run for analysis.
|
2
|
+
#
|
3
|
+
# Each engine must have an `image_name` and `description`.
|
4
|
+
#
|
5
|
+
# When a repo has files that match the `enable_regexps`, that engine will be
|
6
|
+
# enabled by default in the codeclimate.yml file. That file will also have in it
|
7
|
+
# the `default_ratings_paths` globs, which are used during analysis to determine
|
8
|
+
# which files should be rated.
|
9
|
+
#
|
10
|
+
rubocop:
|
11
|
+
image_name: codeclimate/codeclimate-rubocop
|
12
|
+
image_id: 41273519bce24e59685106ff3f4519abc20233fdaf46093576cefecc093b795f
|
13
|
+
description: A Ruby static code analyzer, based on the community Ruby style guide.
|
14
|
+
community: false
|
15
|
+
enable_regexps:
|
16
|
+
- \.rb$
|
17
|
+
default_ratings_paths:
|
18
|
+
- "**.rb"
|
19
|
+
gofmt:
|
20
|
+
image_name: codeclimate/codeclimate-gofmt
|
21
|
+
image_id: fe7b20a9eda6e0bd154fbbb289f0d18ff3ffc41d891cd3b2e5dbe7e015180396
|
22
|
+
description: gofmt
|
23
|
+
community: true
|
24
|
+
enable_regexps:
|
25
|
+
- \.go$
|
26
|
+
default_ratings_paths:
|
27
|
+
- "**.go"
|
28
|
+
golint:
|
29
|
+
image_name: codeclimate/codeclimate-golint
|
30
|
+
image_id: cfde0099114d8b18bec8419d9ed359efac03069821cb967144b807d02ada1412
|
31
|
+
description: golint
|
32
|
+
community: true
|
33
|
+
enable_regexps:
|
34
|
+
- \.go$
|
35
|
+
default_ratings_paths:
|
36
|
+
- "**.go"
|
37
|
+
govet:
|
38
|
+
image_name: codeclimate/codeclimate-govet
|
39
|
+
image_id: fe7690da7198c569a28b69189909eb4c1199d2987bf50ab5d3bbd20d83139489
|
40
|
+
description: govet
|
41
|
+
community: true
|
42
|
+
enable_regexps:
|
43
|
+
- \.go$
|
44
|
+
default_ratings_paths:
|
45
|
+
- "**.go"
|
46
|
+
coffeelint:
|
47
|
+
image_name: codeclimate/codeclimate-coffeelint
|
48
|
+
description: A style checker for CoffeeScript
|
49
|
+
community: false
|
50
|
+
enable_regexps:
|
51
|
+
- \.coffee$
|
52
|
+
default_ratings_paths:
|
53
|
+
- "**.coffee"
|
54
|
+
eslint:
|
55
|
+
image_name: codeclimate/codeclimate-eslint
|
56
|
+
image_id: 28a9ec7c5fc88a67317b6193c3ddc72f1402ba2500e8ed3c6feab6d06ca91f65
|
57
|
+
description: A JavaScript/JSX linting utility
|
58
|
+
community: false
|
59
|
+
enable_regexps:
|
60
|
+
- \.js$
|
61
|
+
- \.jsx$
|
62
|
+
default_ratings_paths:
|
63
|
+
- "**.js"
|
64
|
+
- "**.jsx"
|
65
|
+
csslint:
|
66
|
+
image_name: codeclimate/codeclimate-csslint
|
67
|
+
image_id: 3d5aea2e5f241ff1daff33bd86cc5a819a34c81471e31ca70992f6de5e8edf43
|
68
|
+
description: Automated linting of Cascading Stylesheets
|
69
|
+
community: false
|
70
|
+
enable_regexps:
|
71
|
+
- \.css$
|
72
|
+
default_ratings_paths:
|
73
|
+
- "**.css"
|
74
|
+
watson:
|
75
|
+
image_name: codeclimate/codeclimate-watson
|
76
|
+
description: A young Ember Doctor to help you fix your code.
|
77
|
+
community: true
|
78
|
+
enable_regexps:
|
79
|
+
default_ratings_paths:
|
80
|
+
- "app/**"
|
81
|
+
rubymotion:
|
82
|
+
image_name: codeclimate/codeclimate-rubymotion
|
83
|
+
description: Rubymotion-specific rubocop checks
|
84
|
+
community: true
|
85
|
+
enable_regexps:
|
86
|
+
default_ratings_paths:
|
87
|
+
- "**.rb"
|
88
|
+
bundler-audit:
|
89
|
+
image_name: codeclimate/codeclimate-bundler-audit
|
90
|
+
description: Patch-level verification for Bundler
|
91
|
+
community: false
|
92
|
+
enable_patterns:
|
93
|
+
- Gemfile.lock
|
94
|
+
default_ratings_paths:
|
95
|
+
- Gemfile.lock
|
data/lib/cc/analyzer.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module CC
|
2
|
+
module Analyzer
|
3
|
+
autoload :Accumulator, "cc/analyzer/accumulator"
|
4
|
+
autoload :Config, "cc/analyzer/config"
|
5
|
+
autoload :Engine, "cc/analyzer/engine"
|
6
|
+
autoload :EngineClient, "cc/analyzer/engine_client"
|
7
|
+
autoload :EngineProcess, "cc/analyzer/engine_process"
|
8
|
+
autoload :EngineRegistry, "cc/analyzer/engine_registry"
|
9
|
+
autoload :Filesystem, "cc/analyzer/filesystem"
|
10
|
+
autoload :Formatters, "cc/analyzer/formatters"
|
11
|
+
autoload :IssueSorter, "cc/analyzer/issue_sorter"
|
12
|
+
autoload :LocationDescription,"cc/analyzer/location_description"
|
13
|
+
autoload :NullConfig, "cc/analyzer/null_config"
|
14
|
+
autoload :SourceBuffer, "cc/analyzer/source_buffer"
|
15
|
+
autoload :UnitName, "cc/analyzer/unit_name"
|
16
|
+
|
17
|
+
class DummyStatsd
|
18
|
+
def method_missing(*args)
|
19
|
+
yield if block_given?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
cattr_accessor :statsd
|
24
|
+
self.statsd = DummyStatsd.new
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module CC
|
4
|
+
module Analyzer
|
5
|
+
class Config
|
6
|
+
|
7
|
+
def initialize(config_body)
|
8
|
+
@config = YAML.safe_load(config_body) || {"engines"=> {} }
|
9
|
+
@config["engines"] ||= {}
|
10
|
+
|
11
|
+
expand_shorthand
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_hash
|
15
|
+
@config
|
16
|
+
end
|
17
|
+
|
18
|
+
def engine_config(engine_name)
|
19
|
+
@config["engines"][engine_name] || {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def engine_names
|
23
|
+
@config["engines"].keys.select { |name| engine_enabled?(name) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def engine_present?(engine_name)
|
27
|
+
@config["engines"][engine_name].present?
|
28
|
+
end
|
29
|
+
|
30
|
+
def engine_enabled?(engine_name)
|
31
|
+
@config["engines"][engine_name] && @config["engines"][engine_name]["enabled"]
|
32
|
+
end
|
33
|
+
|
34
|
+
def enable_engine(engine_name)
|
35
|
+
if engine_present?(engine_name)
|
36
|
+
@config["engines"][engine_name]["enabled"] = true
|
37
|
+
else
|
38
|
+
@config["engines"][engine_name] = { "enabled" => true }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def exclude_paths
|
43
|
+
@config["exclude_paths"]
|
44
|
+
end
|
45
|
+
|
46
|
+
def disable_engine(engine_name)
|
47
|
+
if engine_present?(engine_name) && engine_enabled?(engine_name)
|
48
|
+
@config["engines"][engine_name]["enabled"] = false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def remove_engine(engine_name)
|
53
|
+
if engine_present?(engine_name)
|
54
|
+
@config["engines"].delete(engine_name)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_yaml
|
59
|
+
@config.to_yaml
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def expand_shorthand
|
65
|
+
@config["engines"].each do |name, engine_config|
|
66
|
+
if [true, false].include?(engine_config)
|
67
|
+
@config["engines"][name] = { "enabled" => engine_config }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require "posix/spawn"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
module CC
|
5
|
+
module Analyzer
|
6
|
+
class Engine
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
TIMEOUT = 15 * 60 # 15m
|
10
|
+
|
11
|
+
def initialize(name, metadata, code_path, config_json, label)
|
12
|
+
@name = name
|
13
|
+
@metadata = metadata
|
14
|
+
@code_path = code_path
|
15
|
+
@config_json = config_json
|
16
|
+
@label = label.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def run(stdout_io, stderr_io = StringIO.new)
|
20
|
+
pid, _, out, err = POSIX::Spawn.popen4(*docker_run_command)
|
21
|
+
engine_running = true
|
22
|
+
|
23
|
+
t_out = Thread.new do
|
24
|
+
out.each_line("\0") do |chunk|
|
25
|
+
stdout_io.write(chunk.chomp("\0"))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
t_err = Thread.new do
|
30
|
+
err.each_line do |line|
|
31
|
+
if stderr_io
|
32
|
+
stderr_io.write(line)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
Thread.new do
|
38
|
+
sleep TIMEOUT
|
39
|
+
|
40
|
+
if engine_running
|
41
|
+
Thread.current.abort_on_exception = true
|
42
|
+
run_command("docker kill #{container_name}")
|
43
|
+
|
44
|
+
raise EngineTimeout, "engine #{name} ran past #{TIMEOUT} seconds and was killed"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
pid, status = Process.waitpid2(pid)
|
49
|
+
engine_running = false
|
50
|
+
Analyzer.statsd.increment("cli.engines.finished")
|
51
|
+
|
52
|
+
if status.success?
|
53
|
+
Analyzer.statsd.increment("cli.engines.names.#{name}.result.success")
|
54
|
+
Analyzer.statsd.increment("cli.engines.result.success")
|
55
|
+
else
|
56
|
+
Analyzer.statsd.increment("cli.engines.names.#{name}.result.error")
|
57
|
+
Analyzer.statsd.increment("cli.engines.result.error")
|
58
|
+
|
59
|
+
raise EngineFailure, "engine #{name} failed with status #{status.exitstatus} and stderr #{stderr_io.string.inspect}"
|
60
|
+
end
|
61
|
+
ensure
|
62
|
+
t_out.join if t_out
|
63
|
+
t_err.join if t_err
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def container_name
|
69
|
+
@container_name ||= "cc-engines-#{name}-#{SecureRandom.uuid}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def docker_run_command
|
73
|
+
[
|
74
|
+
"docker", "run",
|
75
|
+
"--rm",
|
76
|
+
"--cap-drop", "all",
|
77
|
+
"--label", "com.codeclimate.label=#{@label}",
|
78
|
+
"--name", container_name,
|
79
|
+
"--memory", 512_000_000.to_s, # bytes
|
80
|
+
"--memory-swap", "-1",
|
81
|
+
"--net", "none",
|
82
|
+
"--volume", "#{@code_path}:/code:ro",
|
83
|
+
"--env-file", env_file,
|
84
|
+
@metadata["image_name"],
|
85
|
+
@metadata["command"], # String or Array
|
86
|
+
].flatten.compact
|
87
|
+
end
|
88
|
+
|
89
|
+
def env_file
|
90
|
+
path = File.join("/tmp/cc", SecureRandom.uuid)
|
91
|
+
File.write(path, "ENGINE_CONFIG=#{@config_json}")
|
92
|
+
path
|
93
|
+
end
|
94
|
+
|
95
|
+
def run_command(command)
|
96
|
+
spawn = POSIX::Spawn::Child.new(command)
|
97
|
+
|
98
|
+
unless spawn.status.success?
|
99
|
+
raise CommandFailure, "command '#{command}' failed with status #{spawn.status.exitstatus} and output #{spawn.err}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
CommandFailure = Class.new(StandardError)
|
104
|
+
EngineFailure = Class.new(StandardError)
|
105
|
+
EngineTimeout = Class.new(StandardError)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "safe_yaml"
|
2
|
+
|
3
|
+
module CC
|
4
|
+
module Analyzer
|
5
|
+
class EngineRegistry
|
6
|
+
def initialize
|
7
|
+
@path = File.expand_path("../../../../config/engines.yml", __FILE__)
|
8
|
+
@config = YAML.safe_load_file(@path)
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](engine_name)
|
12
|
+
@config[engine_name]
|
13
|
+
end
|
14
|
+
|
15
|
+
def list
|
16
|
+
@config
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module CC
|
2
|
+
module Analyzer
|
3
|
+
class Filesystem
|
4
|
+
|
5
|
+
def initialize(root)
|
6
|
+
@root = root
|
7
|
+
end
|
8
|
+
|
9
|
+
def exist?(path)
|
10
|
+
File.exist?(path_for(path))
|
11
|
+
end
|
12
|
+
|
13
|
+
def source_buffer_for(path)
|
14
|
+
SourceBuffer.new(path, read_path(path))
|
15
|
+
end
|
16
|
+
|
17
|
+
def read_path(path)
|
18
|
+
File.read(path_for(path))
|
19
|
+
end
|
20
|
+
|
21
|
+
def file_paths
|
22
|
+
Dir.chdir(@root) do
|
23
|
+
Dir["**/*.*"].select { |path| File.file?(path) }.sort
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def all
|
28
|
+
@files ||= Dir.chdir(@root) { Dir.glob("**/*") }
|
29
|
+
end
|
30
|
+
|
31
|
+
def any?(&block)
|
32
|
+
all.any?(&block)
|
33
|
+
end
|
34
|
+
|
35
|
+
def files_matching(globs)
|
36
|
+
Dir.chdir(@root) do
|
37
|
+
globs.map do |glob|
|
38
|
+
Dir.glob(glob)
|
39
|
+
end.flatten.sort.uniq
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def path_for(path)
|
44
|
+
File.join(@root, path)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module CC
|
2
|
+
module Analyzer
|
3
|
+
module Formatters
|
4
|
+
autoload :Formatter, "cc/analyzer/formatters/formatter"
|
5
|
+
autoload :JSONFormatter, "cc/analyzer/formatters/json_formatter"
|
6
|
+
autoload :PlainTextFormatter, "cc/analyzer/formatters/plain_text_formatter"
|
7
|
+
autoload :Spinner, "cc/analyzer/formatters/spinner"
|
8
|
+
|
9
|
+
FORMATTERS = {
|
10
|
+
json: JSONFormatter,
|
11
|
+
text: PlainTextFormatter,
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
def self.resolve(name)
|
15
|
+
FORMATTERS[name.to_sym].new or raise InvalidFormatterError, "'#{name}' is not a valid formatter. Valid options are: #{FORMATTERS.keys.join(", ")}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module CC
|
2
|
+
module Analyzer
|
3
|
+
module Formatters
|
4
|
+
class Formatter
|
5
|
+
def initialize(output = $stdout)
|
6
|
+
@output = output
|
7
|
+
end
|
8
|
+
|
9
|
+
def write(data)
|
10
|
+
end
|
11
|
+
|
12
|
+
def started
|
13
|
+
end
|
14
|
+
|
15
|
+
def engine_running(engine)
|
16
|
+
yield
|
17
|
+
end
|
18
|
+
|
19
|
+
def finished
|
20
|
+
end
|
21
|
+
|
22
|
+
def failed(output)
|
23
|
+
end
|
24
|
+
|
25
|
+
InvalidFormatterError = Class.new(StandardError)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|