cleo_quality_review 0.1.0
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 +7 -0
- data/cleo_quality_review.gemspec +31 -0
- data/config/default.yml +7 -0
- data/exe/check_quality +20 -0
- data/lib/cleo_quality_review/changes_diff.rb +67 -0
- data/lib/cleo_quality_review/checks/debride.rb +65 -0
- data/lib/cleo_quality_review/checks/fasterer.rb +35 -0
- data/lib/cleo_quality_review/checks/flog.rb +35 -0
- data/lib/cleo_quality_review/checks/quality_check.rb +143 -0
- data/lib/cleo_quality_review/checks/reek.rb +53 -0
- data/lib/cleo_quality_review/checks/registry.rb +72 -0
- data/lib/cleo_quality_review/checks.rb +38 -0
- data/lib/cleo_quality_review/cli.rb +105 -0
- data/lib/cleo_quality_review/command_result.rb +21 -0
- data/lib/cleo_quality_review/command_runner.rb +27 -0
- data/lib/cleo_quality_review/configuration.rb +193 -0
- data/lib/cleo_quality_review/diff_map.rb +95 -0
- data/lib/cleo_quality_review/formatter.rb +58 -0
- data/lib/cleo_quality_review/github_review_builder.rb +140 -0
- data/lib/cleo_quality_review/github_review_publisher.rb +150 -0
- data/lib/cleo_quality_review/llm_client.rb +59 -0
- data/lib/cleo_quality_review/llm_config.rb +40 -0
- data/lib/cleo_quality_review/llm_errors.rb +19 -0
- data/lib/cleo_quality_review/llm_logger.rb +66 -0
- data/lib/cleo_quality_review/llm_providers/open_ai.rb +188 -0
- data/lib/cleo_quality_review/llm_providers/open_ai_config.rb +83 -0
- data/lib/cleo_quality_review/llm_providers/registry.rb +61 -0
- data/lib/cleo_quality_review/llm_providers/stub.rb +107 -0
- data/lib/cleo_quality_review/llm_providers.rb +44 -0
- data/lib/cleo_quality_review/options.rb +171 -0
- data/lib/cleo_quality_review/prompt_builder.rb +95 -0
- data/lib/cleo_quality_review/prompt_loader.rb +49 -0
- data/lib/cleo_quality_review/result.rb +58 -0
- data/lib/cleo_quality_review/run.rb +78 -0
- data/lib/cleo_quality_review/run_artifacts/raw_check_outputs.rb +97 -0
- data/lib/cleo_quality_review/run_artifacts.rb +146 -0
- data/lib/cleo_quality_review/runner.rb +158 -0
- data/lib/cleo_quality_review/target_resolver.rb +127 -0
- data/lib/cleo_quality_review/version.rb +7 -0
- data/lib/cleo_quality_review.rb +23 -0
- data/prompts/agent.md +53 -0
- data/prompts/github.md +29 -0
- data/prompts/human.md +23 -0
- data/prompts/pr_review.md +62 -0
- metadata +141 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 63b36db131462e8bedbfe7e7f1d42084a4f600464a6a6b881231a9254d0f4e5b
|
|
4
|
+
data.tar.gz: 5722693b1abeaee01930f71c0e3d5d57333677c2d62bd3f4eaffc613094e3e7b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 31cf5bfcac10c05eb3f1297b132e60b1b730c951c382351879750f51362419df464c103f368e3571dfc6545070f37281827e477cf80af07420d08c8a3a7e4e37
|
|
7
|
+
data.tar.gz: 956cba96beddc95e78ec88b27d0c7e2b25f0b94dd997f8a5ee8b137ab2a34ee7be42e75fb18300fdd5abfc5260978b428d280e6359f730c0cad8b406c58f40cc
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/cleo_quality_review/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "cleo_quality_review"
|
|
7
|
+
spec.version = CleoQualityReview::VERSION
|
|
8
|
+
spec.authors = ["Gavin Morrice"]
|
|
9
|
+
spec.email = ["gavin@gavinmorrice.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Local Cleo code quality checks"
|
|
12
|
+
spec.description = "Runs local quality checks and summarizes their output for humans, agents, or GitHub."
|
|
13
|
+
spec.license = "MIT"
|
|
14
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
15
|
+
|
|
16
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
17
|
+
|
|
18
|
+
spec.files =
|
|
19
|
+
Dir.glob("{#{File.basename(__FILE__)},config/**/*,exe/**/*,lib/**/*,prompts/**/*}", File::FNM_DOTMATCH).select do |path|
|
|
20
|
+
File.file?(path) && !File.symlink?(path)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
spec.bindir = "exe"
|
|
24
|
+
spec.executables = ["check_quality"]
|
|
25
|
+
spec.require_paths = ["lib"]
|
|
26
|
+
|
|
27
|
+
spec.add_dependency "debride"
|
|
28
|
+
spec.add_dependency "fasterer"
|
|
29
|
+
spec.add_dependency "flog"
|
|
30
|
+
spec.add_dependency "reek"
|
|
31
|
+
end
|
data/config/default.yml
ADDED
data/exe/check_quality
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
$LOAD_PATH << File.expand_path('../lib', __dir__)
|
|
4
|
+
|
|
5
|
+
require "cleo_quality_review/cli"
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
# Load development environment variables
|
|
9
|
+
# This should not be used when packaging in production
|
|
10
|
+
if ENV.fetch('CLEO_QUALITY_REVIEW_DEV', 'false') == 'true'
|
|
11
|
+
begin
|
|
12
|
+
require 'dotenv'
|
|
13
|
+
local_dotenv = File.expand_path("../.env.development.local", __dir__)
|
|
14
|
+
Dotenv.load(local_dotenv)
|
|
15
|
+
rescue LoadError
|
|
16
|
+
puts "No local .env.development found. Skip."
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
exit CleoQualityReview::CLI.new(ARGV).run
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
require_relative "target_resolver"
|
|
6
|
+
|
|
7
|
+
module CleoQualityReview
|
|
8
|
+
##
|
|
9
|
+
# Captures the git diff used for a quality review run
|
|
10
|
+
class ChangesDiff
|
|
11
|
+
##
|
|
12
|
+
# @param [Array<String>] target_files files included in the review
|
|
13
|
+
# @param [CommandRunner] command_runner for executing git commands
|
|
14
|
+
def initialize(target_files:, command_runner:)
|
|
15
|
+
@target_files = target_files
|
|
16
|
+
@command_runner = command_runner
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# @return [String] combined tracked and untracked diff content
|
|
21
|
+
def to_s
|
|
22
|
+
@to_s ||= [tracked_changes_diff, untracked_changes_diff].reject(&:empty?).join("\n")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# @return [String] deterministic review identifier for this diff
|
|
27
|
+
def review_id
|
|
28
|
+
Digest::SHA256.hexdigest(to_s)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :command_runner, :target_files
|
|
34
|
+
|
|
35
|
+
def tracked_changes_diff
|
|
36
|
+
command = ["git", "diff", diff_base]
|
|
37
|
+
command.concat(["--", *target_files]) unless target_files.empty?
|
|
38
|
+
|
|
39
|
+
command_runner.run(*command).stdout
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def untracked_changes_diff
|
|
43
|
+
untracked_target_files.map do |filepath|
|
|
44
|
+
command_runner.run("git", "diff", "--no-index", "--", "/dev/null", filepath).stdout
|
|
45
|
+
end.reject(&:empty?).join("\n")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def untracked_target_files
|
|
49
|
+
command = ["git", "ls-files", "--others", "--exclude-standard"]
|
|
50
|
+
empty_targets = target_files.empty?
|
|
51
|
+
command.concat(["--", *target_files]) unless empty_targets
|
|
52
|
+
|
|
53
|
+
command_runner.run(*command).stdout.lines.map(&:strip).select do |path|
|
|
54
|
+
empty_targets || target_files.include?(path)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def diff_base
|
|
59
|
+
@diff_base ||= begin
|
|
60
|
+
result = command_runner.run("git", "merge-base", TargetResolver::BASE_REF, "HEAD")
|
|
61
|
+
base = result.stdout.strip
|
|
62
|
+
|
|
63
|
+
result.success? && !base.empty? ? base : TargetResolver::BASE_REF
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "quality_check"
|
|
6
|
+
|
|
7
|
+
module CleoQualityReview
|
|
8
|
+
module Checks
|
|
9
|
+
##
|
|
10
|
+
# Quality check implementation for Debride unused-code analyzer
|
|
11
|
+
class Debride < QualityCheck
|
|
12
|
+
self.check_name = "dead_code"
|
|
13
|
+
self.tool_name = "debride"
|
|
14
|
+
self.output_extension = "json"
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def command(files)
|
|
19
|
+
[ruby_executable, gem_executable("debride", "debride"), "--json", "--rails", *files]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parse(stdout, stderr)
|
|
23
|
+
findings = missing_methods(stdout).flat_map do |class_name, methods|
|
|
24
|
+
results_for_class(class_name, methods)
|
|
25
|
+
end
|
|
26
|
+
return findings unless findings.empty? && stderr.to_s.strip != ""
|
|
27
|
+
|
|
28
|
+
[result(check: "Execution error", message: stderr, filepath: nil)]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def missing_methods(stdout)
|
|
32
|
+
parsed = JSON.parse(stdout.to_s)
|
|
33
|
+
missing = parsed.fetch("missing", {})
|
|
34
|
+
return {} unless missing.is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
missing
|
|
37
|
+
rescue JSON::ParserError
|
|
38
|
+
{}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def results_for_class(class_name, methods)
|
|
42
|
+
Array(methods).map { |entry| method_to_result(class_name, entry) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def method_to_result(class_name, entry)
|
|
46
|
+
method_name, location = Array(entry)
|
|
47
|
+
filepath, line = parse_location(location)
|
|
48
|
+
|
|
49
|
+
result(
|
|
50
|
+
check: "PotentialDeadMethod",
|
|
51
|
+
message: "#{class_name}##{method_name} might not be called",
|
|
52
|
+
filepath: filepath,
|
|
53
|
+
line: line,
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def parse_location(location)
|
|
58
|
+
match = location.to_s.match(/\A(?<filepath>.*):(?<line>\d+)(?:-\d+)?\z/)
|
|
59
|
+
return [nil, nil] unless match
|
|
60
|
+
|
|
61
|
+
match.values_at(:filepath, :line)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "quality_check"
|
|
4
|
+
|
|
5
|
+
module CleoQualityReview
|
|
6
|
+
module Checks
|
|
7
|
+
##
|
|
8
|
+
# Quality check implementation for Fasterer performance analyzer
|
|
9
|
+
class Fasterer < QualityCheck
|
|
10
|
+
self.check_name = "fasterer"
|
|
11
|
+
self.tool_name = "fasterer"
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def command(files)
|
|
16
|
+
[ruby_executable, gem_executable("fasterer", "fasterer"), *files]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parse(stdout, stderr)
|
|
20
|
+
findings = stdout.to_s.lines.filter_map { |line| parse_line(line) }
|
|
21
|
+
return findings unless findings.empty? && stderr.to_s.strip != ""
|
|
22
|
+
|
|
23
|
+
[result(check: "Execution error", message: stderr, filepath: nil)]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def parse_line(line)
|
|
27
|
+
match = line.to_s.gsub(/\e\[[\d;]*m/, "").match(/^(?<filepath>.+?):(?<line>\d+):?\s+(?<message>.+)$/)
|
|
28
|
+
return unless match
|
|
29
|
+
|
|
30
|
+
filepath, line_number, message = match.values_at(:filepath, :line, :message)
|
|
31
|
+
result(check: "Performance", message: message, filepath: filepath, line: line_number)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "quality_check"
|
|
4
|
+
|
|
5
|
+
module CleoQualityReview
|
|
6
|
+
module Checks
|
|
7
|
+
##
|
|
8
|
+
# Quality check implementation for Flog complexity analyzer
|
|
9
|
+
class Flog < QualityCheck
|
|
10
|
+
self.check_name = "flog"
|
|
11
|
+
self.tool_name = "flog"
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def command(files)
|
|
16
|
+
[ruby_executable, gem_executable("flog", "flog"), "--all", "--methods", *files]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parse(stdout, stderr)
|
|
20
|
+
findings = stdout.to_s.lines.filter_map { |line| parse_line(line) }
|
|
21
|
+
return findings unless findings.empty? && stderr.to_s.strip != ""
|
|
22
|
+
|
|
23
|
+
[result(check: "Execution error", message: stderr, filepath: nil)]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def parse_line(line)
|
|
27
|
+
match = line.match(/^\s*(?<score>\d+(?:\.\d+)?):\s+(?<subject>.+?)\s+(?<filepath>[^:\s]+):(?<line>\d+)/)
|
|
28
|
+
return unless match
|
|
29
|
+
|
|
30
|
+
score, subject, filepath, line_number = match.values_at(:score, :subject, :filepath, :line)
|
|
31
|
+
result(check: "Complexity", message: "#{score}: #{subject}", filepath: filepath, line: line_number)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
require_relative "../result"
|
|
6
|
+
|
|
7
|
+
module CleoQualityReview
|
|
8
|
+
module Checks
|
|
9
|
+
##
|
|
10
|
+
# Base class for quality check implementations
|
|
11
|
+
class QualityCheck
|
|
12
|
+
class << self
|
|
13
|
+
##
|
|
14
|
+
# @!attribute [rw] check_name
|
|
15
|
+
# @return [String] identifier for this check
|
|
16
|
+
attr_accessor :check_name
|
|
17
|
+
|
|
18
|
+
# @!attribute [rw] tool_name
|
|
19
|
+
# @return [String] tool name for result attribution
|
|
20
|
+
attr_accessor :tool_name
|
|
21
|
+
|
|
22
|
+
# @!attribute [rw] tool_type
|
|
23
|
+
# @return [String] category for this tool's findings
|
|
24
|
+
attr_accessor :tool_type
|
|
25
|
+
|
|
26
|
+
# @!attribute [rw] output_extension
|
|
27
|
+
# @return [String] file extension for raw output
|
|
28
|
+
attr_accessor :output_extension
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# Set default output extension for subclasses
|
|
32
|
+
# @param [Class] subclass the inheriting class
|
|
33
|
+
# @return [void]
|
|
34
|
+
def inherited(subclass)
|
|
35
|
+
super
|
|
36
|
+
subclass.output_extension = "txt"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def output_metadata
|
|
40
|
+
{
|
|
41
|
+
check_name: check_name,
|
|
42
|
+
tool_name: tool_name,
|
|
43
|
+
tool_type: tool_type,
|
|
44
|
+
extension: output_extension,
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# @param [CommandRunner] command_runner for executing shell commands
|
|
51
|
+
# @param [Integer] timestamp epoch milliseconds for the run
|
|
52
|
+
def initialize(command_runner:, timestamp:)
|
|
53
|
+
@command_runner = command_runner
|
|
54
|
+
@timestamp = timestamp
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Run the quality check on the given files
|
|
59
|
+
# @param [Array<String>] files file paths to analyze
|
|
60
|
+
# @return [CheckOutput]
|
|
61
|
+
def run(files)
|
|
62
|
+
return empty_output if files.empty?
|
|
63
|
+
|
|
64
|
+
command_result = command_runner.run(*command(files))
|
|
65
|
+
build_output(
|
|
66
|
+
raw_output: raw_output(command_result),
|
|
67
|
+
results: parse(command_result.stdout, parseable_stderr(command_result)),
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
attr_reader :command_runner, :timestamp
|
|
74
|
+
|
|
75
|
+
def check_metadata
|
|
76
|
+
@check_metadata ||= self.class.output_metadata
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def empty_output
|
|
80
|
+
build_output(raw_output: "", results: [])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def build_output(raw_output:, results:)
|
|
84
|
+
CheckOutput.new(
|
|
85
|
+
**check_metadata,
|
|
86
|
+
raw_output: raw_output,
|
|
87
|
+
results: results,
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def ruby_executable
|
|
92
|
+
RbConfig.ruby
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def gem_executable(gem_name, executable_name)
|
|
96
|
+
Gem.bin_path(gem_name, executable_name)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def raw_output(command_result)
|
|
100
|
+
stdout = command_result.stdout
|
|
101
|
+
return stdout if command_result.success?
|
|
102
|
+
|
|
103
|
+
stderr = command_result.stderr
|
|
104
|
+
return stdout if stderr.empty?
|
|
105
|
+
|
|
106
|
+
[stdout, stderr].reject(&:empty?).join("\n")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def parseable_stderr(command_result)
|
|
110
|
+
command_result.success? ? "" : command_result.stderr
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def result(attributes)
|
|
114
|
+
check, message, filepath, line = attributes.values_at(:check, :message, :filepath, :line)
|
|
115
|
+
CleoQualityReview::Result.new(
|
|
116
|
+
**check_metadata.slice(:tool_name, :tool_type),
|
|
117
|
+
check: check,
|
|
118
|
+
timestamp: timestamp,
|
|
119
|
+
result: message,
|
|
120
|
+
filepath: filepath,
|
|
121
|
+
line: line&.to_i,
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
##
|
|
127
|
+
# Value object containing check output and parsed results
|
|
128
|
+
#
|
|
129
|
+
# @!attribute [r] check_name
|
|
130
|
+
# @return [String] identifier for the check
|
|
131
|
+
# @!attribute [r] tool_name
|
|
132
|
+
# @return [String] name of the concrete tool
|
|
133
|
+
# @!attribute [r] tool_type
|
|
134
|
+
# @return [String] category for this tool's findings
|
|
135
|
+
# @!attribute [r] extension
|
|
136
|
+
# @return [String] file extension for the raw output
|
|
137
|
+
# @!attribute [r] raw_output
|
|
138
|
+
# @return [String] raw tool output
|
|
139
|
+
# @!attribute [r] results
|
|
140
|
+
# @return [Array<Result>] parsed findings
|
|
141
|
+
CheckOutput = Struct.new(:check_name, :tool_name, :tool_type, :extension, :raw_output, :results, keyword_init: true)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "quality_check"
|
|
6
|
+
|
|
7
|
+
module CleoQualityReview
|
|
8
|
+
module Checks
|
|
9
|
+
##
|
|
10
|
+
# Quality check implementation for Reek code smell detector
|
|
11
|
+
class Reek < QualityCheck
|
|
12
|
+
self.check_name = "reek"
|
|
13
|
+
self.tool_name = "reek"
|
|
14
|
+
self.output_extension = "json"
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def command(files)
|
|
19
|
+
[ruby_executable, gem_executable("reek", "reek"), "--format", "json", *files]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parse(stdout, stderr)
|
|
23
|
+
smells = parse_json(stdout)
|
|
24
|
+
return stderr_result(stderr) if smells.empty? && stderr.to_s.strip != ""
|
|
25
|
+
|
|
26
|
+
smells.map { |smell| smell_to_result(smell) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def smell_to_result(smell)
|
|
30
|
+
result(
|
|
31
|
+
check: smell.fetch("smell_type", "Reek"),
|
|
32
|
+
message: smell_message(smell),
|
|
33
|
+
filepath: smell.fetch("source", nil),
|
|
34
|
+
line: Array(smell["lines"]).first,
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse_json(stdout)
|
|
39
|
+
JSON.parse(stdout.to_s)
|
|
40
|
+
rescue JSON::ParserError
|
|
41
|
+
[]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def smell_message(smell)
|
|
45
|
+
[smell["context"], smell["message"]].compact.join(": ")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def stderr_result(stderr)
|
|
49
|
+
[result(check: "Execution error", message: stderr, filepath: nil)]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CleoQualityReview
|
|
4
|
+
module Checks
|
|
5
|
+
##
|
|
6
|
+
# Registry for available quality check implementations
|
|
7
|
+
class Registry
|
|
8
|
+
class UnknownCheckError < CleoQualityReview::Error
|
|
9
|
+
def initialize(name)
|
|
10
|
+
super("Unknown check name: #{name}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require 'delegate'
|
|
15
|
+
class CheckName < DelegateClass(String)
|
|
16
|
+
def initialize(name)
|
|
17
|
+
super(name.to_s.downcase)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Registration = Data.define(:check_name, :klass, :tool_type)
|
|
23
|
+
class << self
|
|
24
|
+
##
|
|
25
|
+
# Register a quality check implementation
|
|
26
|
+
# @param [String] name check identifier
|
|
27
|
+
# @param [Class] klass class name under CleoQualityReview
|
|
28
|
+
# @param [Symbol, String] tool_type category of tool findings
|
|
29
|
+
# @return [void]
|
|
30
|
+
def register(name, klass, tool_type:)
|
|
31
|
+
name = CheckName.new(name).to_s
|
|
32
|
+
registration = Registration.new(check_name: name.to_s, klass: klass, tool_type: tool_type.to_s)
|
|
33
|
+
registrations[name] = registration
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# Resolve check names to check classes
|
|
39
|
+
# @param [Array<String>] names check names to resolve
|
|
40
|
+
# @return [Array<Class>] resolved check classes
|
|
41
|
+
# @raise [ArgumentError] if an unknown check name is provided
|
|
42
|
+
def resolve(names)
|
|
43
|
+
names_to_resolve(names).map { |name| resolve_name(name) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def registered?(tool_name)
|
|
47
|
+
registrations.key?(CheckName.new(tool_name))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def registrations
|
|
53
|
+
@registrations ||= {}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def names_to_resolve(names)
|
|
57
|
+
names = ['all'] if names.empty?
|
|
58
|
+
|
|
59
|
+
names.include?('all') ? registrations.keys : names.map(&:to_s)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def resolve_name(name)
|
|
63
|
+
name = CheckName.new(name).to_s
|
|
64
|
+
registration = registrations[name]
|
|
65
|
+
raise UnknownCheckError.new(name) unless registration
|
|
66
|
+
|
|
67
|
+
registration.klass
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CleoQualityReview
|
|
4
|
+
##
|
|
5
|
+
# Namespace for bundled quality check implementations.
|
|
6
|
+
module Checks
|
|
7
|
+
require_relative "checks/registry"
|
|
8
|
+
require_relative "checks/quality_check"
|
|
9
|
+
require_relative "checks/reek"
|
|
10
|
+
require_relative "checks/flog"
|
|
11
|
+
require_relative "checks/fasterer"
|
|
12
|
+
require_relative "checks/debride"
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
##
|
|
16
|
+
# Register a new check for use
|
|
17
|
+
# @param [String] tool_name
|
|
18
|
+
# @param [Class] tool_class
|
|
19
|
+
# @param [String, Symbol] tool_type
|
|
20
|
+
# @return [nil]
|
|
21
|
+
def register(tool_name, tool_class, tool_type: )
|
|
22
|
+
Registry.register(tool_name.to_s, tool_class, tool_type: tool_type.to_s)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def resolve(tool_names)
|
|
26
|
+
Registry.resolve(Array(tool_names))
|
|
27
|
+
end
|
|
28
|
+
##
|
|
29
|
+
# Has a tool with the given name been registered?
|
|
30
|
+
#
|
|
31
|
+
# @param [String] tool_name
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
def registered?(tool_name)
|
|
34
|
+
Registry.registered?(tool_name)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
require_relative "../cleo_quality_review"
|
|
6
|
+
require_relative "command_runner"
|
|
7
|
+
require_relative "formatter"
|
|
8
|
+
require_relative "github_review_publisher"
|
|
9
|
+
require_relative "options"
|
|
10
|
+
require_relative "runner"
|
|
11
|
+
require_relative "run_artifacts"
|
|
12
|
+
|
|
13
|
+
module CleoQualityReview
|
|
14
|
+
##
|
|
15
|
+
# Command-line interface entry point
|
|
16
|
+
class CLI
|
|
17
|
+
SUBCOMMANDS = {
|
|
18
|
+
"analyze" => :run_analyze,
|
|
19
|
+
"render" => :run_render,
|
|
20
|
+
"publish-pr-review" => :run_publish_pr_review,
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# @param [Array<String>] argv command-line arguments
|
|
25
|
+
# @param [IO] stdout standard output stream
|
|
26
|
+
# @param [IO] stderr standard error stream
|
|
27
|
+
def initialize(argv, stdout: $stdout, stderr: $stderr)
|
|
28
|
+
@argv = argv
|
|
29
|
+
@stdout = stdout
|
|
30
|
+
@stderr = stderr
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# Execute the CLI
|
|
35
|
+
# @return [Integer] exit code (0 for success, 1 for error)
|
|
36
|
+
def run
|
|
37
|
+
dispatch_command
|
|
38
|
+
rescue Error, OptionParser::ParseError, ArgumentError => error
|
|
39
|
+
stderr.puts("check_quality: #{error.message}")
|
|
40
|
+
1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def dispatch_command
|
|
46
|
+
@command_runner = CommandRunner.new
|
|
47
|
+
command = argv.first
|
|
48
|
+
subcommand = SUBCOMMANDS[command]
|
|
49
|
+
|
|
50
|
+
if subcommand
|
|
51
|
+
run_subcommand(subcommand, argv.drop(1))
|
|
52
|
+
else
|
|
53
|
+
run_one_shot(argv)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
attr_reader :argv, :stdout, :stderr, :command_runner
|
|
58
|
+
|
|
59
|
+
def run_one_shot(arguments)
|
|
60
|
+
options = Options.parse(arguments)
|
|
61
|
+
run = Runner.new(options: options, command_runner: command_runner).run
|
|
62
|
+
output = Formatter.new(run: run, command_runner: command_runner).format
|
|
63
|
+
print_output(output)
|
|
64
|
+
0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def run_subcommand(subcommand, arguments)
|
|
68
|
+
send(subcommand, arguments)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def run_analyze(arguments)
|
|
72
|
+
options = Options.parse(arguments)
|
|
73
|
+
run = Runner.new(options: options, command_runner: command_runner).run
|
|
74
|
+
stdout.puts(run.review_id)
|
|
75
|
+
0
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def run_render(arguments)
|
|
79
|
+
options = Options.parse(arguments)
|
|
80
|
+
run = RunArtifacts.load(review_id: options.validated_review_id).to_run(**options.run_loading_params)
|
|
81
|
+
output = Formatter.new(run: run, command_runner: command_runner).format
|
|
82
|
+
print_output(output)
|
|
83
|
+
0
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def run_publish_pr_review(arguments)
|
|
87
|
+
options = Options.parse(arguments)
|
|
88
|
+
run = RunArtifacts.load(review_id: options.validated_review_id).to_run(**options.run_loading_params)
|
|
89
|
+
output = GitHubReviewPublisher.new(run: run, rendered_review: rendered_pr_review(options, run)).publish
|
|
90
|
+
print_output(output)
|
|
91
|
+
0
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def print_output(output)
|
|
95
|
+
stdout.puts(output) unless output.empty?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def rendered_pr_review(options, run)
|
|
99
|
+
path = options.review_file || File.join(run.run_directory, "pr_review.json")
|
|
100
|
+
raise OptionParser::MissingArgument, "--review-file is required or #{path} must exist" unless File.file?(path)
|
|
101
|
+
|
|
102
|
+
File.read(path)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|