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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 52d0e1b2249535049c9a049fe4c8955bd0e6478e0c1b852f52a1cece489ac6d8
|
|
4
|
+
data.tar.gz: 6577bb1134e0e78115148bfb130fcd0d236410080917cc5203f3bd199a088970
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9290ef1235939702c2a31c7588bc5a8302f2ca994b75e2b39998fe669941094be0cf0eb58bb50cd4e504555167e1e34767ec96524644c5c6aa9859e1efb0a2ba
|
|
7
|
+
data.tar.gz: a7f3bc181cd224ed873ff5f991b7bff30a320c59b741dd82dd8ef87f99b32cd845e5b6d128ab46593235e06e53963488f02e025b32af3f6f7e4f180bba9377f0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Salvador Fuentes Jr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubocop"
|
|
4
|
+
|
|
5
|
+
module MetzScan
|
|
6
|
+
module Analyzers
|
|
7
|
+
class ContextualNodeWalker
|
|
8
|
+
ContextualNode = Struct.new(:node, :enclosing_name, :method_name, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
Context = Struct.new(:namespace, :method_name, keyword_init: true) do
|
|
11
|
+
def self.root = new(namespace: [], method_name: nil)
|
|
12
|
+
|
|
13
|
+
def enclosing_name = namespace.join("::").then { |name| name unless name.empty? }
|
|
14
|
+
|
|
15
|
+
def with_namespace(name) = self.class.new(namespace: namespace + [name].compact, method_name: nil)
|
|
16
|
+
|
|
17
|
+
def with_method(name) = self.class.new(namespace: namespace, method_name: name)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(root)
|
|
21
|
+
@root = root
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def nodes
|
|
25
|
+
collect(root, Context.root, [])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
attr_reader :root
|
|
31
|
+
|
|
32
|
+
def collect(node, context, nodes)
|
|
33
|
+
return nodes unless node
|
|
34
|
+
return collect_scope(node, context, nodes) if scope_node?(node)
|
|
35
|
+
|
|
36
|
+
collect_current(node, context, nodes)
|
|
37
|
+
collect_children(node, context, nodes)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def collect_scope(node, context, nodes)
|
|
41
|
+
return collect_namespace(node, context, nodes) if namespace_node?(node)
|
|
42
|
+
return collect_method(node, context, nodes) if node.type == :def
|
|
43
|
+
|
|
44
|
+
collect_singleton_method(node, context, nodes)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def collect_namespace(node, context, nodes)
|
|
48
|
+
collect(node.children.last, context.with_namespace(constant_name(node.children.first)), nodes)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def collect_method(node, context, nodes)
|
|
52
|
+
collect(node.children[2], context.with_method("##{node.method_name}"), nodes)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def collect_singleton_method(node, context, nodes)
|
|
56
|
+
collect(node.children[3], context.with_method(".#{node.method_name}"), nodes)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def collect_current(node, context, nodes)
|
|
60
|
+
nodes << ContextualNode.new(node: node, enclosing_name: context.enclosing_name,
|
|
61
|
+
method_name: context.method_name)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def collect_children(node, context, nodes)
|
|
65
|
+
node.children.grep(RuboCop::AST::Node).each { |child| collect(child, context, nodes) }
|
|
66
|
+
nodes
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def scope_node?(node)
|
|
70
|
+
namespace_node?(node) || %i[def defs].include?(node.type)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def namespace_node?(node)
|
|
74
|
+
%i[class module].include?(node.type)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def constant_name(node)
|
|
78
|
+
node.source if node&.type == :const
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MetzScan
|
|
4
|
+
module Analyzers
|
|
5
|
+
module CrossPackageReferenceMetadata
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def counts(reference_set)
|
|
9
|
+
{ "referring_file_count" => reference_set.files.size,
|
|
10
|
+
"referring_package_count" => reference_set.packages.size,
|
|
11
|
+
"referring_packages" => reference_set.packages,
|
|
12
|
+
"reference_shape" => reference_shape(reference_set) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def reference_shape(reference_set)
|
|
16
|
+
{ "referring_file_count" => reference_set.files.size,
|
|
17
|
+
"referring_package_count" => reference_set.packages.size,
|
|
18
|
+
"referring_package_roots" => reference_set.package_roots,
|
|
19
|
+
"referring_package_leafs" => reference_set.package_leafs }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def declaration_metadata(declaration)
|
|
23
|
+
{ "name" => declaration.name, "kind" => declaration.kind.to_s, "path" => declaration.path }.compact
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def references_metadata(reference_set)
|
|
27
|
+
reference_set.references.map { |reference| reference_metadata(reference) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reference_metadata(reference)
|
|
31
|
+
{ "path" => reference.path, "line" => reference.line, "column" => reference.column,
|
|
32
|
+
"package" => reference.package }.compact
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MetzScan
|
|
4
|
+
module Analyzers
|
|
5
|
+
class CrossPackageReferenceSet
|
|
6
|
+
def initialize(references)
|
|
7
|
+
@references = references
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
attr_reader :references
|
|
11
|
+
|
|
12
|
+
def files
|
|
13
|
+
references.map(&:path).uniq.sort
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def packages
|
|
17
|
+
references.map(&:package).uniq.sort
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def package_roots
|
|
21
|
+
packages.map { |package| package.to_s.split("/").first }.uniq.sort
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def package_leafs
|
|
25
|
+
packages.map { |package| package.to_s.split("/").last }.uniq.sort
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def enough?(minimum_files, minimum_packages)
|
|
29
|
+
files.size >= minimum_files && packages.size >= minimum_packages
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubocop"
|
|
4
|
+
|
|
5
|
+
require_relative "../contextual_node_walker"
|
|
6
|
+
require_relative "ambient_context_reference"
|
|
7
|
+
require_relative "current_attribute_collector"
|
|
8
|
+
require_relative "thread_current_collector"
|
|
9
|
+
|
|
10
|
+
module MetzScan
|
|
11
|
+
module Analyzers
|
|
12
|
+
class ImplicitContextPressure
|
|
13
|
+
class AmbientContextCollector
|
|
14
|
+
def initialize(path)
|
|
15
|
+
@path = path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
contextual_nodes.flat_map { |contextual_node| references_for(contextual_node) }
|
|
20
|
+
rescue Parser::SyntaxError
|
|
21
|
+
[]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :path
|
|
27
|
+
|
|
28
|
+
def references_for(contextual_node)
|
|
29
|
+
reference_collectors.filter_map { |collector| collector.reference_for(contextual_node) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reference_collectors
|
|
33
|
+
[current_attribute_collector, thread_current_collector]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def current_attribute_collector
|
|
37
|
+
@current_attribute_collector ||= CurrentAttributeCollector.new(path)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def thread_current_collector
|
|
41
|
+
@thread_current_collector ||= ThreadCurrentCollector.new(path)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def processed_source
|
|
45
|
+
RuboCop::ProcessedSource.new(File.read(path), RUBY_VERSION.to_f)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def contextual_nodes
|
|
49
|
+
ContextualNodeWalker.new(processed_source.ast).nodes
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MetzScan
|
|
4
|
+
module Analyzers
|
|
5
|
+
class ImplicitContextPressure
|
|
6
|
+
class AmbientContextCollector
|
|
7
|
+
Reference = Struct.new(:ambient_context, :kind, :context_key, :attribute, :access_mode,
|
|
8
|
+
:enclosing_name, :method_name, :path, :line, :expression,
|
|
9
|
+
keyword_init: true) do
|
|
10
|
+
def context
|
|
11
|
+
return "#{enclosing_name}#{method_name}" if enclosing_name && method_name
|
|
12
|
+
|
|
13
|
+
method_name || enclosing_name
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module ReferenceMetadata
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :path
|
|
21
|
+
|
|
22
|
+
def context_attributes(contextual_node)
|
|
23
|
+
{ enclosing_name: contextual_node.enclosing_name, method_name: contextual_node.method_name }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def location_attributes(node)
|
|
27
|
+
{ path: path, line: node.loc.expression.line, expression: first_line(node) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def first_line(node)
|
|
31
|
+
node.loc.expression.source.lines.first.strip
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MetzScan
|
|
4
|
+
module Analyzers
|
|
5
|
+
class ImplicitContextPressure
|
|
6
|
+
class AmbientContextCollector
|
|
7
|
+
class CurrentAttributeCollector
|
|
8
|
+
include ReferenceMetadata
|
|
9
|
+
|
|
10
|
+
ATTRIBUTE_METHOD = /\A[a-z_]\w*=?\z/
|
|
11
|
+
IGNORED_CURRENT_METHODS = %w[after_reset attributes before_reset reset resets set].freeze
|
|
12
|
+
private_constant :ATTRIBUTE_METHOD, :IGNORED_CURRENT_METHODS
|
|
13
|
+
|
|
14
|
+
def initialize(path)
|
|
15
|
+
@path = path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def reference_for(contextual_node)
|
|
19
|
+
node = contextual_node.node
|
|
20
|
+
return unless current_attribute_reference?(node)
|
|
21
|
+
|
|
22
|
+
Reference.new(reference_attributes(contextual_node))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def reference_attributes(contextual_node)
|
|
28
|
+
node = contextual_node.node
|
|
29
|
+
current_attribute_attributes(node)
|
|
30
|
+
.merge(context_attributes(contextual_node))
|
|
31
|
+
.merge(location_attributes(node))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def current_attribute_attributes(node)
|
|
35
|
+
attribute = attribute_name(node.method_name)
|
|
36
|
+
{ ambient_context: "#{node.receiver.source}.#{attribute}", kind: "current_attributes",
|
|
37
|
+
context_key: attribute, attribute: attribute, access_mode: access_mode(node.method_name) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def current_attribute_reference?(node)
|
|
41
|
+
node.type == :send && current_receiver?(node.receiver) && attribute_method?(node)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def current_receiver?(node)
|
|
45
|
+
node&.type == :const && node.source.split("::").last == "Current"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def attribute_method?(node)
|
|
49
|
+
method_name = node.method_name.to_s
|
|
50
|
+
method_name.match?(ATTRIBUTE_METHOD) && counted_current_method?(method_name, node.arguments)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def counted_current_method?(method_name, arguments)
|
|
54
|
+
!IGNORED_CURRENT_METHODS.include?(attribute_name(method_name)) &&
|
|
55
|
+
valid_attribute_argument_count?(method_name, arguments)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def valid_attribute_argument_count?(method_name, arguments)
|
|
59
|
+
method_name.end_with?("=") ? arguments.size == 1 : arguments.empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def attribute_name(method_name)
|
|
63
|
+
method_name.to_s.delete_suffix("=")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def access_mode(method_name)
|
|
67
|
+
method_name.to_s.end_with?("=") ? "write" : "read"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../package_map"
|
|
4
|
+
|
|
5
|
+
module MetzScan
|
|
6
|
+
module Analyzers
|
|
7
|
+
class ImplicitContextPressure
|
|
8
|
+
class ReferenceSet
|
|
9
|
+
def initialize(references)
|
|
10
|
+
@references = references.sort_by { |reference| [reference.path, reference.line.to_i] }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :references
|
|
14
|
+
|
|
15
|
+
def referring_files
|
|
16
|
+
references.map(&:path).uniq
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def referring_packages
|
|
20
|
+
references.map { |reference| PackageMap.package_for(reference.path) }.compact.uniq.sort
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def access_modes
|
|
24
|
+
references.map(&:access_mode).uniq.sort
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def context_kind
|
|
28
|
+
references.first.kind
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def context_key
|
|
32
|
+
references.first.context_key
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MetzScan
|
|
4
|
+
module Analyzers
|
|
5
|
+
class ImplicitContextPressure
|
|
6
|
+
class AmbientContextCollector
|
|
7
|
+
class ThreadCurrentCollector
|
|
8
|
+
include ReferenceMetadata
|
|
9
|
+
|
|
10
|
+
THREAD_KEY_READ = :[]
|
|
11
|
+
THREAD_KEY_WRITE = :[]=
|
|
12
|
+
private_constant :THREAD_KEY_READ, :THREAD_KEY_WRITE
|
|
13
|
+
|
|
14
|
+
def initialize(path)
|
|
15
|
+
@path = path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def reference_for(contextual_node)
|
|
19
|
+
node = contextual_node.node
|
|
20
|
+
return unless thread_current_reference?(node)
|
|
21
|
+
|
|
22
|
+
Reference.new(reference_attributes(contextual_node))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def reference_attributes(contextual_node)
|
|
28
|
+
node = contextual_node.node
|
|
29
|
+
thread_current_attributes(node)
|
|
30
|
+
.merge(context_attributes(contextual_node))
|
|
31
|
+
.merge(location_attributes(node))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def thread_current_attributes(node)
|
|
35
|
+
{ ambient_context: thread_current_context(node), kind: "thread_current",
|
|
36
|
+
context_key: literal_thread_key(node.arguments.first), access_mode: access_mode(node) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def thread_current_reference?(node)
|
|
40
|
+
thread_key_access?(node) && thread_current_receiver?(node.receiver) &&
|
|
41
|
+
literal_thread_key(node.arguments.first)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def thread_key_access?(node)
|
|
45
|
+
return false unless node.type == :send
|
|
46
|
+
return node.arguments.size == 1 if node.method_name == THREAD_KEY_READ
|
|
47
|
+
return node.arguments.size == 2 if node.method_name == THREAD_KEY_WRITE
|
|
48
|
+
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def thread_current_receiver?(node)
|
|
53
|
+
node&.type == :send && node.method_name == :current &&
|
|
54
|
+
node.arguments.empty? && thread_constant?(node.receiver)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def thread_constant?(node)
|
|
58
|
+
node&.type == :const && node.source == "Thread"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def literal_thread_key(node)
|
|
62
|
+
node.value.to_s if node && %i[sym str].include?(node.type)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def thread_current_context(node)
|
|
66
|
+
"Thread.current[#{node.arguments.first.source}]"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def access_mode(node)
|
|
70
|
+
node.method_name == THREAD_KEY_WRITE ? "write" : "read"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MetzScan
|
|
4
|
+
module Analyzers
|
|
5
|
+
class ImplicitContextPressure
|
|
6
|
+
module Triage
|
|
7
|
+
CATEGORY_TRIAGE = {
|
|
8
|
+
"root_current_read" => {
|
|
9
|
+
triage_summary: "Candidate application Current signal; review whether callers should receive context.",
|
|
10
|
+
why: "Application-wide Current reads hide dependencies from method signatures and make workflows " \
|
|
11
|
+
"harder to reason about.",
|
|
12
|
+
suggested_next_moves: [
|
|
13
|
+
"Pass the context value explicitly into collaborators that use it.",
|
|
14
|
+
"Keep request-scoped globals at application boundaries when possible."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"root_current_write" => {
|
|
18
|
+
triage_summary: "Candidate mutable application Current signal; review whether writes should stay at " \
|
|
19
|
+
"request boundaries.",
|
|
20
|
+
why: "Mutable application Current access hides both dependency and mutation order from the callers " \
|
|
21
|
+
"that depend on it.",
|
|
22
|
+
suggested_next_moves: [
|
|
23
|
+
"Keep Current writes near request, job, or tenant setup boundaries.",
|
|
24
|
+
"Pass context explicitly once domain collaborators need the value."
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"namespaced_current_read" => {
|
|
28
|
+
triage_summary: "Candidate namespaced Current signal; review whether the namespace is leaking ambient " \
|
|
29
|
+
"context.",
|
|
30
|
+
why: "Namespaced Current reads can make a package depend on ambient state owned by another namespace.",
|
|
31
|
+
suggested_next_moves: [
|
|
32
|
+
"Pass the namespaced context value into collaborators that use it repeatedly.",
|
|
33
|
+
"Keep namespace-owned Current access behind the namespace boundary when possible."
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"namespaced_current_write" => {
|
|
37
|
+
triage_summary: "Candidate namespaced Current signal with writes; review whether mutation should stay " \
|
|
38
|
+
"at namespace boundaries.",
|
|
39
|
+
why: "Namespaced Current writes spread mutation order across packages and make the ambient context " \
|
|
40
|
+
"harder to audit.",
|
|
41
|
+
suggested_next_moves: [
|
|
42
|
+
"Keep namespaced Current writes near the boundary that establishes the namespace context.",
|
|
43
|
+
"Pass explicit context to downstream collaborators instead of writing ambient state mid-workflow."
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"thread_current_read" => {
|
|
47
|
+
triage_summary: "Candidate Thread.current signal; review whether thread-local dependencies should be " \
|
|
48
|
+
"explicit.",
|
|
49
|
+
why: "Repeated thread-local reads hide dependencies from method signatures and make context setup " \
|
|
50
|
+
"harder to audit.",
|
|
51
|
+
suggested_next_moves: [
|
|
52
|
+
"Pass the thread-local value explicitly into collaborators that use it repeatedly.",
|
|
53
|
+
"Keep thread-local reads at framework, request, or instrumentation boundaries when possible."
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"thread_current_write" => {
|
|
57
|
+
triage_summary: "Candidate mutable Thread.current signal; review whether thread-local writes should " \
|
|
58
|
+
"stay at setup boundaries.",
|
|
59
|
+
why: "Repeated thread-local writes hide mutation order in ambient state and make cleanup requirements " \
|
|
60
|
+
"harder to audit.",
|
|
61
|
+
suggested_next_moves: [
|
|
62
|
+
"Keep Thread.current writes near setup/teardown boundaries that own the thread-local lifecycle.",
|
|
63
|
+
"Pass explicit context to downstream collaborators instead of mutating thread-local state mid-flow."
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}.freeze
|
|
67
|
+
private_constant :CATEGORY_TRIAGE
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def implicit_context_category_for(ambient_context, grouped)
|
|
72
|
+
return "thread_current_#{access_category_for(grouped)}" if grouped.context_kind == "thread_current"
|
|
73
|
+
|
|
74
|
+
"#{current_receiver_scope_for(ambient_context)}_current_#{access_category_for(grouped)}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def current_receiver_scope_for(ambient_context)
|
|
78
|
+
ambient_context.include?("::Current.") ? "namespaced" : "root"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def current_attribute_for(ambient_context)
|
|
82
|
+
ambient_context.split(".").last
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def access_category_for(grouped)
|
|
86
|
+
grouped.access_modes.include?("write") ? "write" : "read"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def access_phrase(grouped)
|
|
90
|
+
return "read and written" if mixed_access?(grouped)
|
|
91
|
+
return "written" if grouped.access_modes.include?("write")
|
|
92
|
+
|
|
93
|
+
"read"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def mixed_access?(grouped)
|
|
97
|
+
grouped.access_modes.include?("read") && grouped.access_modes.include?("write")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def category_triage_attributes(category)
|
|
101
|
+
profile = CATEGORY_TRIAGE.fetch(category)
|
|
102
|
+
|
|
103
|
+
{ triage_summary: profile.fetch(:triage_summary), why_it_matters: profile.fetch(:why),
|
|
104
|
+
suggested_next_moves: profile.fetch(:suggested_next_moves) }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|