rubocop-metz 0.5.2
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.
Potentially problematic release.
This version of rubocop-metz might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/lib/metz_scan/analyzers/contextual_node_walker.rb +82 -0
- data/lib/metz_scan/analyzers/cross_package_reference_metadata.rb +36 -0
- data/lib/metz_scan/analyzers/cross_package_reference_set.rb +33 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/ambient_context_collector.rb +54 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/ambient_context_reference.rb +37 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/current_attribute_collector.rb +73 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/reference_set.rb +37 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/thread_current_collector.rb +76 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/triage.rb +109 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure.rb +139 -0
- data/lib/metz_scan/analyzers/inheritance_descendants/finding.rb +18 -0
- data/lib/metz_scan/analyzers/inheritance_descendants/metadata.rb +23 -0
- data/lib/metz_scan/analyzers/inheritance_descendants/root_kind.rb +58 -0
- data/lib/metz_scan/analyzers/inheritance_descendants/triage.rb +30 -0
- data/lib/metz_scan/analyzers/inheritance_descendants.rb +137 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/finding.rb +20 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/metadata.rb +40 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/namespace.rb +41 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/reference_collector.rb +50 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/reference_set.rb +12 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/shared_namespace_triage/exception_declaration.rb +117 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/shared_namespace_triage.rb +69 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure.rb +139 -0
- data/lib/metz_scan/analyzers/occurrence.rb +28 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/finding.rb +20 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/metadata.rb +27 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/package_map.rb +11 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/reference_collector.rb +38 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/reference_set.rb +12 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/shared_dependency_triage.rb +124 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure.rb +139 -0
- data/lib/metz_scan/analyzers/package_map.rb +81 -0
- data/lib/metz_scan/analyzers/project_analyzer_triage.rb +20 -0
- data/lib/metz_scan/analyzers/repeated_branching/branch_site_collector.rb +138 -0
- data/lib/metz_scan/analyzers/repeated_branching/branch_value.rb +27 -0
- data/lib/metz_scan/analyzers/repeated_branching/contextual_node_walker.rb +11 -0
- data/lib/metz_scan/analyzers/repeated_branching/decision_subject.rb +46 -0
- data/lib/metz_scan/analyzers/repeated_branching/predicate_chain.rb +58 -0
- data/lib/metz_scan/analyzers/repeated_branching/triage.rb +28 -0
- data/lib/metz_scan/analyzers/repeated_branching.rb +135 -0
- data/lib/metz_scan/analyzers/repeated_query_criteria/triage.rb +103 -0
- data/lib/metz_scan/analyzers/repeated_query_criteria.rb +285 -0
- data/lib/metz_scan/analyzers/ruby_file_enumerator.rb +37 -0
- data/lib/metz_scan/analyzers/service_soup/service_call_pattern.rb +53 -0
- data/lib/metz_scan/analyzers/service_soup/setup_workflow_triage.rb +48 -0
- data/lib/metz_scan/analyzers/service_soup/workflow_collector.rb +130 -0
- data/lib/metz_scan/analyzers/service_soup.rb +128 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure/family_builder.rb +69 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure/finding.rb +30 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure/metadata.rb +74 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure/method_body_facts.rb +100 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure/triage.rb +106 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure.rb +133 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/artifact_writer.rb +51 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/baseline_delta.rb +231 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/collaborators.rb +76 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/markdown_renderer.rb +353 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/readiness_catalog.rb +123 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/summary.rb +333 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/target_set.rb +139 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner.rb +64 -0
- data/lib/metz_scan/calibration/rubydex_drift_formatter.rb +91 -0
- data/lib/metz_scan/cli.rb +120 -0
- data/lib/metz_scan/commands/explain.rb +128 -0
- data/lib/metz_scan/commands/project_analyzers.rb +105 -0
- data/lib/metz_scan/commands/report.rb +115 -0
- data/lib/metz_scan/commands/rules.rb +92 -0
- data/lib/metz_scan/commands/scan/auto_fix.rb +138 -0
- data/lib/metz_scan/commands/scan/compliance_scorecard.rb +124 -0
- data/lib/metz_scan/commands/scan/github_annotations_renderer.rb +55 -0
- data/lib/metz_scan/commands/scan/offense_extractor.rb +42 -0
- data/lib/metz_scan/commands/scan/project_analyzer_breakdown.rb +55 -0
- data/lib/metz_scan/commands/scan/project_analyzer_metadata.rb +78 -0
- data/lib/metz_scan/commands/scan/project_analyzer_offenses.rb +85 -0
- data/lib/metz_scan/commands/scan/project_analyzer_runner.rb +148 -0
- data/lib/metz_scan/commands/scan/project_analyzer_summary_aggregate_formatter.rb +74 -0
- data/lib/metz_scan/commands/scan/project_analyzer_summary_breakdown_formatter.rb +46 -0
- data/lib/metz_scan/commands/scan/project_analyzer_triage_formatter.rb +40 -0
- data/lib/metz_scan/commands/scan/project_analyzer_triage_priority.rb +38 -0
- data/lib/metz_scan/commands/scan/project_config_scope.rb +231 -0
- data/lib/metz_scan/commands/scan/runner.rb +199 -0
- data/lib/metz_scan/commands/scan/sarif_renderer.rb +103 -0
- data/lib/metz_scan/commands/scan/sarif_rule_descriptors.rb +82 -0
- data/lib/metz_scan/commands/scan/sarif_severity.rb +27 -0
- data/lib/metz_scan/commands/scan/target_ruby_version.rb +56 -0
- data/lib/metz_scan/commands/scan/text_renderer.rb +145 -0
- data/lib/metz_scan/commands/scan.rb +201 -0
- data/lib/metz_scan/project_index/null_backend.rb +35 -0
- data/lib/metz_scan/project_index/rubydex_backend/file_discovery.rb +33 -0
- data/lib/metz_scan/project_index/rubydex_backend/location_formatting.rb +36 -0
- data/lib/metz_scan/project_index/rubydex_backend/method_declarations.rb +118 -0
- data/lib/metz_scan/project_index/rubydex_backend.rb +133 -0
- data/lib/metz_scan/project_index.rb +109 -0
- data/lib/metz_scan/version.rb +5 -0
- data/lib/metz_scan.rb +5 -0
- metadata +185 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
require_relative "../version"
|
|
7
|
+
require_relative "scan/compliance_scorecard"
|
|
8
|
+
require_relative "scan/runner"
|
|
9
|
+
require_relative "scan/project_analyzer_runner"
|
|
10
|
+
require_relative "scan/text_renderer"
|
|
11
|
+
require_relative "scan/sarif_renderer"
|
|
12
|
+
require_relative "scan/github_annotations_renderer"
|
|
13
|
+
require_relative "scan/auto_fix"
|
|
14
|
+
|
|
15
|
+
module MetzScan
|
|
16
|
+
module Commands
|
|
17
|
+
class ScanOptions
|
|
18
|
+
USAGE = "Usage: metz-scan scan PATH... [--format text|json|sarif|gh-annotations] " \
|
|
19
|
+
"[--all-cops] [--project-analyzers] [--auto-fix [--unsafe] [--dry-run]]"
|
|
20
|
+
VALID_FORMATS = %w[text json sarif gh-annotations].freeze
|
|
21
|
+
DEFAULT_FORMAT = "text"
|
|
22
|
+
|
|
23
|
+
Options = Struct.new(:format, :paths, :all_cops, :project_analyzers, :auto_fix, :unsafe, :dry_run, :help,
|
|
24
|
+
keyword_init: true)
|
|
25
|
+
|
|
26
|
+
def self.parse(argv)
|
|
27
|
+
new.parse(argv)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.help
|
|
31
|
+
new.help
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def parse(argv)
|
|
35
|
+
flags = default_flags
|
|
36
|
+
paths = option_parser(flags).parse(argv)
|
|
37
|
+
Options.new(**flags, paths: paths)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def help
|
|
41
|
+
option_parser(default_flags).help
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def default_flags
|
|
47
|
+
{ format: DEFAULT_FORMAT, all_cops: false, project_analyzers: false, auto_fix: false, unsafe: false,
|
|
48
|
+
dry_run: false, help: false }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def option_parser(flags)
|
|
52
|
+
OptionParser.new(USAGE) { |opts| configure_parser(opts, flags) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def configure_parser(opts, flags)
|
|
56
|
+
configure_format_parser(opts, flags)
|
|
57
|
+
configure_cop_selection_parser(opts, flags)
|
|
58
|
+
configure_project_analyzer_parser(opts, flags)
|
|
59
|
+
configure_auto_fix_parser(opts, flags)
|
|
60
|
+
configure_help_parser(opts, flags)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def configure_format_parser(opts, flags)
|
|
64
|
+
opts.on("--format FORMAT", "Output format: text (default), json, sarif, gh-annotations") do |format|
|
|
65
|
+
flags[:format] = format
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def configure_cop_selection_parser(opts, flags)
|
|
70
|
+
opts.on("--all-cops", "Run the full RuboCop suite instead of Metz/* cops by default") do
|
|
71
|
+
flags[:all_cops] = true
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def configure_project_analyzer_parser(opts, flags)
|
|
76
|
+
opts.on("--project-analyzers", "Include all opt-in project analyzer findings") do
|
|
77
|
+
flags[:project_analyzers] = true
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def configure_auto_fix_parser(opts, flags)
|
|
82
|
+
opts.on("--auto-fix", "Apply RuboCop's safe fixes (delegates to rubocop -a)") { flags[:auto_fix] = true }
|
|
83
|
+
opts.on("--unsafe", "With --auto-fix, also apply unsafe fixes (rubocop -A)") { flags[:unsafe] = true }
|
|
84
|
+
opts.on("--dry-run", "With --auto-fix, print diff without modifying files") { flags[:dry_run] = true }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def configure_help_parser(opts, flags)
|
|
88
|
+
opts.on("-h", "--help", "Show this help message and exit") { flags[:help] = true }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class Scan
|
|
93
|
+
USAGE = ScanOptions::USAGE
|
|
94
|
+
VALID_FORMATS = ScanOptions::VALID_FORMATS
|
|
95
|
+
RENDERERS = { "sarif" => SarifRenderer, "gh-annotations" => GithubAnnotationsRenderer }.freeze
|
|
96
|
+
|
|
97
|
+
def self.run(argv, stdout:, stderr:)
|
|
98
|
+
new(stdout: stdout, stderr: stderr).run(argv)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def initialize(stdout:, stderr:)
|
|
102
|
+
@stdout = stdout
|
|
103
|
+
@stderr = stderr
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def run(argv)
|
|
107
|
+
handle_options(parse_options(argv))
|
|
108
|
+
rescue OptionParser::ParseError => e
|
|
109
|
+
parser_error(e)
|
|
110
|
+
rescue Runner::Error => e
|
|
111
|
+
runner_error(e)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
attr_reader :stdout, :stderr
|
|
117
|
+
|
|
118
|
+
def handle_options(options)
|
|
119
|
+
return print_help if options.help
|
|
120
|
+
|
|
121
|
+
validate(options) || dispatch(options)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def parse_options(argv)
|
|
125
|
+
ScanOptions.parse(argv)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def validate(options)
|
|
129
|
+
return missing_path_arg if options.paths.empty?
|
|
130
|
+
return missing_paths(options.paths) unless options.paths.all? { |p| File.exist?(p) }
|
|
131
|
+
return nil if options.auto_fix
|
|
132
|
+
|
|
133
|
+
invalid_format(options.format) unless VALID_FORMATS.include?(options.format)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def dispatch(options)
|
|
137
|
+
return AutoFix.new(stdout: stdout, stderr: stderr).run(options) if options.auto_fix
|
|
138
|
+
|
|
139
|
+
scan(options)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def scan(options)
|
|
143
|
+
parsed = Runner.invoke(options.paths, all_cops: options.all_cops, stderr: stderr)
|
|
144
|
+
merge_project_analyzers(parsed, options)
|
|
145
|
+
add_compliance_summary(parsed)
|
|
146
|
+
render(parsed, options.format)
|
|
147
|
+
Runner.exit_code_for(parsed)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def add_compliance_summary(parsed)
|
|
151
|
+
ComplianceScorecard.add_to_summary!(parsed)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def merge_project_analyzers(parsed, options)
|
|
155
|
+
ProjectAnalyzerRunner.merge!(parsed, options.paths, **project_analyzer_options(options))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def project_analyzer_options(options)
|
|
159
|
+
{ default_output: !options.project_analyzers }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def render(parsed, format)
|
|
163
|
+
return stdout.puts JSON.generate(parsed) if format == "json"
|
|
164
|
+
|
|
165
|
+
RENDERERS.fetch(format, TextRenderer).new(stdout, parsed).render
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def parser_error(err)
|
|
169
|
+
stderr.puts "metz-scan scan: #{err.message}"
|
|
170
|
+
stderr.puts USAGE
|
|
171
|
+
1
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def print_help
|
|
175
|
+
stdout.puts ScanOptions.help
|
|
176
|
+
0
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def missing_path_arg
|
|
180
|
+
stderr.puts "metz-scan scan: missing PATH argument."
|
|
181
|
+
stderr.puts USAGE
|
|
182
|
+
1
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def missing_paths(paths)
|
|
186
|
+
paths.reject { |p| File.exist?(p) }.each { |p| stderr.puts "metz-scan scan: no such file or directory: #{p}" }
|
|
187
|
+
1
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def invalid_format(fmt)
|
|
191
|
+
stderr.puts "metz-scan scan: invalid --format '#{fmt}'. Valid formats: text, json, sarif, gh-annotations."
|
|
192
|
+
1
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def runner_error(err)
|
|
196
|
+
stderr.puts "metz-scan scan: RuboCop failed: #{err.message}"
|
|
197
|
+
2
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MetzScan
|
|
4
|
+
class ProjectIndex
|
|
5
|
+
class NullBackend
|
|
6
|
+
def initialize(reason:)
|
|
7
|
+
@reason = reason
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
attr_reader :reason
|
|
11
|
+
|
|
12
|
+
def name = :null
|
|
13
|
+
|
|
14
|
+
def available? = false
|
|
15
|
+
|
|
16
|
+
def indexed_files = []
|
|
17
|
+
|
|
18
|
+
def index_errors = []
|
|
19
|
+
|
|
20
|
+
def diagnostics = []
|
|
21
|
+
|
|
22
|
+
def declarations = []
|
|
23
|
+
|
|
24
|
+
def method_declarations = []
|
|
25
|
+
|
|
26
|
+
def documents = []
|
|
27
|
+
|
|
28
|
+
def descendants_of(_name) = []
|
|
29
|
+
|
|
30
|
+
def constant_references_to(_name) = []
|
|
31
|
+
|
|
32
|
+
def search(_query) = []
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MetzScan
|
|
4
|
+
class ProjectIndex
|
|
5
|
+
class RubydexBackend
|
|
6
|
+
module FileDiscovery
|
|
7
|
+
RUBY_GLOB = "**/*.rb"
|
|
8
|
+
private_constant :RUBY_GLOB
|
|
9
|
+
|
|
10
|
+
def ruby_files_for(paths)
|
|
11
|
+
paths.flat_map { |path| ruby_files_under(path) }.uniq.sort
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def workspace_path_for(paths)
|
|
15
|
+
expanded = File.expand_path(paths.first || Dir.pwd)
|
|
16
|
+
return expanded if File.directory?(expanded)
|
|
17
|
+
|
|
18
|
+
File.dirname(expanded)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def ruby_files_under(path)
|
|
24
|
+
expanded = File.expand_path(path)
|
|
25
|
+
return Dir.glob(File.join(expanded, RUBY_GLOB)) if File.directory?(expanded)
|
|
26
|
+
return [expanded] if File.file?(expanded) && File.extname(expanded) == ".rb"
|
|
27
|
+
|
|
28
|
+
[]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module MetzScan
|
|
6
|
+
class ProjectIndex
|
|
7
|
+
class RubydexBackend
|
|
8
|
+
module LocationFormatting
|
|
9
|
+
def path_from_location(location)
|
|
10
|
+
return unless location
|
|
11
|
+
|
|
12
|
+
path_from_file_location(location) || path_from_uri_location(location) || location.to_s
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def path_from_file_location(location)
|
|
16
|
+
location.to_file_path if location.respond_to?(:to_file_path)
|
|
17
|
+
rescue StandardError
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def path_from_uri_location(location)
|
|
22
|
+
path_from_uri(location.uri) if location.respond_to?(:uri)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def path_from_uri(uri)
|
|
26
|
+
string = uri.to_s
|
|
27
|
+
return URI(string).path if string.start_with?("file:")
|
|
28
|
+
|
|
29
|
+
string
|
|
30
|
+
rescue URI::InvalidURIError
|
|
31
|
+
string
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MetzScan
|
|
4
|
+
class ProjectIndex
|
|
5
|
+
class RubydexBackend
|
|
6
|
+
module MethodDeclarations
|
|
7
|
+
def method_declarations
|
|
8
|
+
method_declaration_entries.sort_by do |declaration|
|
|
9
|
+
[declaration.owner_name.to_s, declaration.method_identity.to_s, declaration.path.to_s,
|
|
10
|
+
declaration.line.to_i]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def method_declaration_entries
|
|
17
|
+
graph.declarations.select { |declaration| method_declaration?(declaration) }
|
|
18
|
+
.filter_map { |declaration| method_declaration_for(declaration) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def method_declaration?(declaration)
|
|
22
|
+
defined?(Rubydex::Method) && declaration.instance_of?(Rubydex::Method)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def method_declaration_for(declaration)
|
|
26
|
+
attributes = method_declaration_attributes(declaration)
|
|
27
|
+
MethodDeclaration.new(**attributes) if attributes
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def method_declaration_attributes(declaration)
|
|
31
|
+
owner_attributes = method_owner_attributes(declaration)
|
|
32
|
+
return unless owner_attributes
|
|
33
|
+
|
|
34
|
+
method_name = method_name_for(owner_attributes.fetch(:signature))
|
|
35
|
+
identity_attributes(declaration, owner_attributes, method_name)
|
|
36
|
+
.merge(location_attributes(declaration))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def method_owner_and_signature(declaration)
|
|
40
|
+
owner_attributes = method_owner_attributes(declaration)
|
|
41
|
+
[owner_attributes[:owner_name], owner_attributes[:signature]] if owner_attributes
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def method_owner_attributes(declaration)
|
|
45
|
+
raw_owner_name = raw_owner_name_for(declaration)
|
|
46
|
+
signature = signature_for(declaration.name, raw_owner_name)
|
|
47
|
+
return unless raw_owner_name && signature
|
|
48
|
+
|
|
49
|
+
{ owner_name: normalized_owner_name(raw_owner_name), signature: signature,
|
|
50
|
+
receiver_kind: receiver_kind_for(raw_owner_name) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def identity_attributes(declaration, owner_attributes, method_name)
|
|
54
|
+
receiver_kind = owner_attributes.fetch(:receiver_kind)
|
|
55
|
+
{ name: declaration.name, owner_name: owner_attributes.fetch(:owner_name), method_name: method_name,
|
|
56
|
+
signature: owner_attributes.fetch(:signature), receiver_kind: receiver_kind,
|
|
57
|
+
method_identity: method_identity_for(receiver_kind, method_name) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def location_attributes(declaration)
|
|
61
|
+
location = definition_display_location(declaration)
|
|
62
|
+
{ path: path_from_location(location), line: line_from_location(location),
|
|
63
|
+
column: column_from_location(location) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def raw_owner_name_for(declaration)
|
|
67
|
+
declaration.owner.name if declaration.respond_to?(:owner) && declaration.owner.respond_to?(:name)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def signature_for(name, owner_name)
|
|
71
|
+
name.to_s.delete_prefix("#{owner_name}#")
|
|
72
|
+
.delete_prefix("#{owner_name}.")
|
|
73
|
+
.delete_prefix("#{owner_name}::")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def method_name_for(signature)
|
|
77
|
+
signature.to_s.sub(/\(.*\)\z/, "")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def receiver_kind_for(raw_owner_name)
|
|
81
|
+
singleton_owner_name?(raw_owner_name) ? "singleton" : "instance"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def normalized_owner_name(raw_owner_name)
|
|
85
|
+
singleton_owner_name?(raw_owner_name) ? singleton_owner_name(raw_owner_name) : raw_owner_name
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def singleton_owner_name?(raw_owner_name)
|
|
89
|
+
singleton_owner_name(raw_owner_name)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def singleton_owner_name(raw_owner_name)
|
|
93
|
+
owner_name, singleton_tail = raw_owner_name.to_s.split("::<", 2)
|
|
94
|
+
return unless singleton_tail&.delete_suffix(">") == owner_name
|
|
95
|
+
|
|
96
|
+
owner_name
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def method_identity_for(receiver_kind, method_name)
|
|
100
|
+
"#{receiver_kind}:#{method_name}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def definition_display_location(declaration)
|
|
104
|
+
definition = declaration.definitions.first if declaration.respond_to?(:definitions)
|
|
105
|
+
display_location_for(definition)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def line_from_location(location)
|
|
109
|
+
location.start_line if location.respond_to?(:start_line)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def column_from_location(location)
|
|
113
|
+
location.start_column if location.respond_to?(:start_column)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rubydex_backend/file_discovery"
|
|
4
|
+
require_relative "rubydex_backend/location_formatting"
|
|
5
|
+
require_relative "rubydex_backend/method_declarations"
|
|
6
|
+
|
|
7
|
+
module MetzScan
|
|
8
|
+
class ProjectIndex
|
|
9
|
+
class RubydexBackend
|
|
10
|
+
include LocationFormatting
|
|
11
|
+
include MethodDeclarations
|
|
12
|
+
extend FileDiscovery
|
|
13
|
+
|
|
14
|
+
DECLARATION_KINDS = { "Rubydex::Class" => :class, "Rubydex::Module" => :module }.freeze
|
|
15
|
+
|
|
16
|
+
def self.available?
|
|
17
|
+
require "rubydex"
|
|
18
|
+
true
|
|
19
|
+
rescue LoadError
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.unavailable_reason
|
|
24
|
+
"rubydex is not installed; enable the optional rubydex bundle group first"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.build(paths, workspace: false)
|
|
28
|
+
require "rubydex"
|
|
29
|
+
|
|
30
|
+
files = ruby_files_for(paths)
|
|
31
|
+
graph = Rubydex::Graph.new(workspace_path: workspace_path_for(paths))
|
|
32
|
+
index_errors = index_graph(graph, files, workspace)
|
|
33
|
+
|
|
34
|
+
new(graph: graph, indexed_files: files, index_errors: index_errors)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.index_graph(graph, files, workspace)
|
|
38
|
+
errors = workspace ? graph.index_workspace : graph.index_all(files)
|
|
39
|
+
graph.resolve
|
|
40
|
+
errors
|
|
41
|
+
end
|
|
42
|
+
private_class_method :index_graph
|
|
43
|
+
|
|
44
|
+
def initialize(graph:, indexed_files:, index_errors:)
|
|
45
|
+
@graph = graph
|
|
46
|
+
@indexed_files = indexed_files
|
|
47
|
+
@index_errors = Array(index_errors)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
attr_reader :graph, :indexed_files, :index_errors
|
|
51
|
+
|
|
52
|
+
def name = :rubydex
|
|
53
|
+
|
|
54
|
+
def available? = true
|
|
55
|
+
|
|
56
|
+
def reason = nil
|
|
57
|
+
|
|
58
|
+
def diagnostics
|
|
59
|
+
graph.diagnostics.to_a
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def declarations
|
|
63
|
+
entries = graph.declarations.map do |declaration|
|
|
64
|
+
Declaration.new(name: declaration.name, path: definition_path(declaration),
|
|
65
|
+
kind: declaration_kind(declaration))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
entries.sort_by { |declaration| [declaration.name.to_s, declaration.path.to_s] }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def documents
|
|
72
|
+
graph.documents.map { |document| path_from_uri(document.uri) }.compact.sort
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def descendants_of(name)
|
|
76
|
+
declaration = graph[name]
|
|
77
|
+
return [] unless declaration.respond_to?(:descendants)
|
|
78
|
+
|
|
79
|
+
declaration.descendants.map(&:name).reject { |descendant| descendant == name }.sort
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def constant_references_to(name)
|
|
83
|
+
declaration = graph[name]
|
|
84
|
+
return [] unless declaration.respond_to?(:references)
|
|
85
|
+
|
|
86
|
+
declaration.references.map { |reference| normalized_reference(reference) }.compact.sort_by do |reference|
|
|
87
|
+
[reference.path.to_s, reference.line.to_i, reference.column.to_i, reference.name.to_s]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def search(query)
|
|
92
|
+
graph.search(query).map(&:name).sort
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def definition_path(declaration)
|
|
98
|
+
definition = declaration.definitions.first if declaration.respond_to?(:definitions)
|
|
99
|
+
path_from_display_location(definition)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def declaration_kind(declaration) = DECLARATION_KINDS[declaration.class.name]
|
|
103
|
+
|
|
104
|
+
def path_from_display_location(definition)
|
|
105
|
+
path_from_location(display_location_for(definition))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def display_location_for(definition)
|
|
109
|
+
location = definition&.location
|
|
110
|
+
return location.to_display if location.respond_to?(:to_display)
|
|
111
|
+
|
|
112
|
+
location
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def normalized_reference(reference)
|
|
116
|
+
location = reference.location.to_display
|
|
117
|
+
Reference.new(**reference_attributes(reference, location))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def reference_attributes(reference, location)
|
|
121
|
+
{ name: reference_name(reference), path: path_from_location(location),
|
|
122
|
+
line: location.start_line, column: location.start_column }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def reference_name(reference)
|
|
126
|
+
return reference.declaration.name if reference.respond_to?(:declaration)
|
|
127
|
+
return reference.name if reference.respond_to?(:name)
|
|
128
|
+
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "project_index/null_backend"
|
|
4
|
+
require_relative "project_index/rubydex_backend"
|
|
5
|
+
|
|
6
|
+
module MetzScan
|
|
7
|
+
# Thin facade over optional project-wide indexes used by future analyzers.
|
|
8
|
+
class ProjectIndex
|
|
9
|
+
Declaration = Struct.new(:name, :path, :kind, keyword_init: true)
|
|
10
|
+
MethodDeclaration = Struct.new(:name, :owner_name, :method_name, :signature, :path, :line, :column,
|
|
11
|
+
:receiver_kind, :method_identity, keyword_init: true)
|
|
12
|
+
Reference = Struct.new(:name, :path, :line, :column, keyword_init: true)
|
|
13
|
+
|
|
14
|
+
class UnknownBackendError < StandardError; end
|
|
15
|
+
class UnavailableBackendError < StandardError; end
|
|
16
|
+
|
|
17
|
+
BACKENDS = %i[auto null rubydex].freeze
|
|
18
|
+
private_constant :BACKENDS
|
|
19
|
+
|
|
20
|
+
def self.build(paths, backend: :auto, workspace: false)
|
|
21
|
+
backend = backend.to_sym
|
|
22
|
+
raise UnknownBackendError, "unknown project index backend: #{backend}" unless BACKENDS.include?(backend)
|
|
23
|
+
|
|
24
|
+
new(build_backend(Array(paths), backend, workspace))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.build_backend(paths, backend, workspace)
|
|
28
|
+
case backend
|
|
29
|
+
when :auto then auto_backend(paths, workspace)
|
|
30
|
+
when :null then null_backend("project index disabled")
|
|
31
|
+
when :rubydex then rubydex_backend(paths, workspace)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
private_class_method :build_backend
|
|
35
|
+
|
|
36
|
+
def self.auto_backend(paths, workspace)
|
|
37
|
+
return rubydex_backend(paths, workspace) if RubydexBackend.available?
|
|
38
|
+
|
|
39
|
+
null_backend(RubydexBackend.unavailable_reason)
|
|
40
|
+
end
|
|
41
|
+
private_class_method :auto_backend
|
|
42
|
+
|
|
43
|
+
def self.rubydex_backend(paths, workspace)
|
|
44
|
+
raise UnavailableBackendError, RubydexBackend.unavailable_reason unless RubydexBackend.available?
|
|
45
|
+
|
|
46
|
+
RubydexBackend.build(paths, workspace: workspace)
|
|
47
|
+
end
|
|
48
|
+
private_class_method :rubydex_backend
|
|
49
|
+
|
|
50
|
+
def self.null_backend(reason)
|
|
51
|
+
NullBackend.new(reason: reason)
|
|
52
|
+
end
|
|
53
|
+
private_class_method :null_backend
|
|
54
|
+
|
|
55
|
+
def initialize(backend)
|
|
56
|
+
@backend = backend
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
attr_reader :backend
|
|
60
|
+
|
|
61
|
+
def backend_name
|
|
62
|
+
backend.name
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def available?
|
|
66
|
+
backend.available?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def reason
|
|
70
|
+
backend.reason
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def indexed_files
|
|
74
|
+
backend.indexed_files
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def index_errors
|
|
78
|
+
backend.index_errors
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def diagnostics
|
|
82
|
+
backend.diagnostics
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def declarations
|
|
86
|
+
backend.declarations
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def method_declarations
|
|
90
|
+
backend.method_declarations
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def documents
|
|
94
|
+
backend.documents
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def descendants_of(name)
|
|
98
|
+
backend.descendants_of(name)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def constant_references_to(name)
|
|
102
|
+
backend.constant_references_to(name)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def search(query)
|
|
106
|
+
backend.search(query)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|