reference_extractor 0.1.0 → 0.1.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/.tool-versions +1 -1
- data/CHANGELOG.md +5 -0
- data/IMPLEMENTATION.md +43 -0
- data/lib/reference_extractor/extractor.rb +6 -4
- data/lib/reference_extractor/internal/association_inspector.rb +76 -0
- data/lib/reference_extractor/internal/ast_reference_extractor.rb +2 -2
- data/lib/reference_extractor/internal/const_node_inspector.rb +1 -1
- data/lib/reference_extractor/internal.rb +1 -0
- data/lib/reference_extractor/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19d808e45b0a49435136cb0813dbc66af70b68f1127c1d40d497b97f77da470c
|
|
4
|
+
data.tar.gz: 9f7419a764a29617ed8d8e33c3783c527a2854e697c8439a83139dbff16268d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7adb979d6be9eee24fc653046c46bfd243f4c762e4afc27b639a9b477ea7fa555662f8a3f649300a51294bdd193f6b83959e53877f3ab03bf8ad2d657648f177
|
|
7
|
+
data.tar.gz: 872d6e9a503e1027bb976a6ec600fc2f1b272f73d5e9fc1fc1dabe8d5300fa59b469fc8562f94904873196fa81b4b6f4d084f33f08108ec687e903b903a58060
|
data/.tool-versions
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby
|
|
1
|
+
ruby 4.0.1
|
data/CHANGELOG.md
CHANGED
data/IMPLEMENTATION.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# ReferenceExtractor Implementation Notes
|
|
2
|
+
|
|
3
|
+
## Initial Implementation Plan
|
|
4
|
+
|
|
5
|
+
Looking back at years of building and using [packwerk](https://github.com/Shopify/packwerk), there are some major things I would like to improve in the next version.
|
|
6
|
+
|
|
7
|
+
Properties I want from ReferenceExtractor:
|
|
8
|
+
|
|
9
|
+
- extract the most complex, least specific part into a reusable module. That is, finding all external references from a Ruby file
|
|
10
|
+
- enabled by the above, allow arbitrary (through extension) rules to be expressed over that foundational reference graph
|
|
11
|
+
- start with layering, the most common architecture rule (_A boundary in software architecture is a line that is crossed by dependencies only in one direction_)
|
|
12
|
+
- optionally and later, future-proof the core
|
|
13
|
+
- make sure it uses a current version of prism in the canonical way for parsing
|
|
14
|
+
- remove the dependency on zeitwerk _or_ go all in on zeitwerk and remove constant_resolver
|
|
15
|
+
|
|
16
|
+
There is a possible version of this where reference_extractor-core is a separate gem.
|
|
17
|
+
|
|
18
|
+
Also, please note that all names are temporary at this point.
|
|
19
|
+
|
|
20
|
+
I am not pushing this as a next version of packwerk though due to two reasons:
|
|
21
|
+
|
|
22
|
+
- Packwerk development has been at a snail's pace for the last few years as Shopify has reduced its ongoing investment to just "keeping the lights on" and I don't have the influence required to change that - I don't want to spend energy arguing, I want to push this out into the world
|
|
23
|
+
- Packwerk's original architecture was based on assumptions that have long been invalidated (e.g. running as a rubocop cop) and it's difficult to remove the remnants of these decisions from its architecture
|
|
24
|
+
- another outdated assumption is that config validation is slow while the actual reference checking is fast. Nowadays both require bootup of the application, so there is actually no need for a separate `validate` command
|
|
25
|
+
- Packwerk has accumulated a lot of complexity to enable less common use cases and add convenience. Those would slow down iteration towards a different paradigm.
|
|
26
|
+
|
|
27
|
+
Relevant open PRs on packwerk:
|
|
28
|
+
|
|
29
|
+
- [packwerk#410](https://github.com/Shopify/packwerk/pull/410) Proof of concept for removing ConstantResolver and using Zeitwerk for ConstantDiscovery
|
|
30
|
+
- [packwerk#397](https://github.com/Shopify/packwerk/pull/397) Allow fetching all references for a file, or all files, using public API
|
|
31
|
+
|
|
32
|
+
## Implemented Improvements over Packwerk
|
|
33
|
+
|
|
34
|
+
- Removed dependency on `constant_resolver` by depending directly on zeitwerk for reverse lookup, thanks to @Catsuko ([packwerk#410](https://github.com/Shopify/packwerk/pull/410))
|
|
35
|
+
- Replaced `better_html` with `herb` which comes with a lot less dependencies
|
|
36
|
+
- remove possibly outdated encoding handling from parsers
|
|
37
|
+
- fixed prism deprecation warnings, thanks to @Earlopain ([packwerk#431](https://github.com/Shopify/packwerk/pull/431))
|
|
38
|
+
|
|
39
|
+
## Ideas
|
|
40
|
+
- Eliminate Rails dependency
|
|
41
|
+
- would be nice to just depend on zeitwerk, not Rails
|
|
42
|
+
- instead of reading from `Rails.autoloaders`, can we just get the autoloaders from Zeitwerk?
|
|
43
|
+
- remove association inspector for now? It may cause more problems than it solves
|
|
@@ -26,7 +26,7 @@ module ReferenceExtractor
|
|
|
26
26
|
ast = parse_ruby_string(snippet)
|
|
27
27
|
return [] unless ast
|
|
28
28
|
|
|
29
|
-
extract_references(ast, relative_path: "<snippet>")
|
|
29
|
+
extract_references(ast, relative_path: Pathname.new("<snippet>"))
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# Extract constant references from a Ruby file.
|
|
@@ -40,7 +40,7 @@ module ReferenceExtractor
|
|
|
40
40
|
ast = parse_file(absolute_path)
|
|
41
41
|
return [] unless ast
|
|
42
42
|
|
|
43
|
-
relative_path = Pathname.new(absolute_path).relative_path_from(root_path)
|
|
43
|
+
relative_path = Pathname.new(absolute_path).relative_path_from(root_path)
|
|
44
44
|
extract_references(ast, relative_path:)
|
|
45
45
|
end
|
|
46
46
|
|
|
@@ -66,8 +66,10 @@ module ReferenceExtractor
|
|
|
66
66
|
|
|
67
67
|
def ast_reference_extractor
|
|
68
68
|
@ast_reference_extractor ||= Internal::AstReferenceExtractor.new(
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
constant_name_inspectors: [
|
|
70
|
+
Internal::ConstNodeInspector.new,
|
|
71
|
+
Internal::AssociationInspector.new
|
|
72
|
+
],
|
|
71
73
|
context_provider: @context_provider,
|
|
72
74
|
root_path:
|
|
73
75
|
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReferenceExtractor
|
|
4
|
+
module Internal
|
|
5
|
+
# Extracts the implicit constant reference from an Active Record association
|
|
6
|
+
class AssociationInspector
|
|
7
|
+
RAILS_ASSOCIATIONS = [
|
|
8
|
+
:belongs_to,
|
|
9
|
+
:has_many,
|
|
10
|
+
:has_one,
|
|
11
|
+
:has_and_belongs_to_many
|
|
12
|
+
].to_set
|
|
13
|
+
|
|
14
|
+
DEFAULT_EXCLUDED_FILES = Set.new([
|
|
15
|
+
"spec/factories/**",
|
|
16
|
+
"test/factories/**"
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
def initialize(
|
|
20
|
+
inflector: ActiveSupport::Inflector,
|
|
21
|
+
custom_associations: Set.new,
|
|
22
|
+
excluded_files: DEFAULT_EXCLUDED_FILES
|
|
23
|
+
)
|
|
24
|
+
@inflector = inflector
|
|
25
|
+
@associations = RAILS_ASSOCIATIONS + custom_associations
|
|
26
|
+
@excluded_files = excluded_files
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def constant_name_from_node(node, ancestors:, relative_path:)
|
|
30
|
+
return unless NodeHelpers.method_call?(node)
|
|
31
|
+
return if excluded?(relative_path)
|
|
32
|
+
return unless association?(node)
|
|
33
|
+
|
|
34
|
+
arguments = NodeHelpers.method_arguments(node)
|
|
35
|
+
association_name = association_name(arguments)
|
|
36
|
+
return unless association_name
|
|
37
|
+
|
|
38
|
+
if (class_name_node = custom_class_name(arguments))
|
|
39
|
+
return unless NodeHelpers.string?(class_name_node)
|
|
40
|
+
|
|
41
|
+
NodeHelpers.literal_value(class_name_node)
|
|
42
|
+
else
|
|
43
|
+
@inflector.classify(association_name.to_s)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def excluded?(relative_file)
|
|
50
|
+
@excluded_files.any? do |pattern|
|
|
51
|
+
relative_file.fnmatch?(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def association?(node)
|
|
56
|
+
method_name = NodeHelpers.method_name(node)
|
|
57
|
+
@associations.include?(method_name)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def custom_class_name(arguments)
|
|
61
|
+
association_options = arguments.detect { |n| NodeHelpers.hash?(n) }
|
|
62
|
+
return unless association_options
|
|
63
|
+
|
|
64
|
+
NodeHelpers.value_from_hash(association_options, :class_name)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def association_name(arguments)
|
|
68
|
+
association_name_node = arguments[0]
|
|
69
|
+
return unless association_name_node
|
|
70
|
+
return unless NodeHelpers.symbol?(association_name_node)
|
|
71
|
+
|
|
72
|
+
NodeHelpers.literal_value(association_name_node)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -18,7 +18,7 @@ module ReferenceExtractor
|
|
|
18
18
|
next if constant.nil?
|
|
19
19
|
|
|
20
20
|
# Ignore references that resolve to the same file they originate from.
|
|
21
|
-
next if constant.location
|
|
21
|
+
next if constant.location == unresolved_reference.relative_path
|
|
22
22
|
|
|
23
23
|
fully_qualified_references << Reference.new(
|
|
24
24
|
relative_path: unresolved_reference.relative_path,
|
|
@@ -57,7 +57,7 @@ module ReferenceExtractor
|
|
|
57
57
|
constant_name = nil
|
|
58
58
|
|
|
59
59
|
@constant_name_inspectors.each do |inspector|
|
|
60
|
-
constant_name = inspector.constant_name_from_node(node, ancestors:)
|
|
60
|
+
constant_name = inspector.constant_name_from_node(node, ancestors:, relative_path:)
|
|
61
61
|
|
|
62
62
|
break if constant_name
|
|
63
63
|
end
|
|
@@ -4,7 +4,7 @@ module ReferenceExtractor
|
|
|
4
4
|
module Internal
|
|
5
5
|
# Extracts a constant name from an AST node of type :const
|
|
6
6
|
class ConstNodeInspector
|
|
7
|
-
def constant_name_from_node(node, ancestors:)
|
|
7
|
+
def constant_name_from_node(node, ancestors:, relative_path: nil)
|
|
8
8
|
return nil unless NodeHelpers.constant?(node)
|
|
9
9
|
|
|
10
10
|
parent = ancestors.first
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: reference_extractor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Theus
|
|
@@ -92,6 +92,7 @@ files:
|
|
|
92
92
|
- ".tool-versions"
|
|
93
93
|
- CHANGELOG.md
|
|
94
94
|
- CODE_OF_CONDUCT.md
|
|
95
|
+
- IMPLEMENTATION.md
|
|
95
96
|
- LICENSE
|
|
96
97
|
- LICENSE.txt
|
|
97
98
|
- README.md
|
|
@@ -101,6 +102,7 @@ files:
|
|
|
101
102
|
- lib/reference_extractor/constant_context.rb
|
|
102
103
|
- lib/reference_extractor/extractor.rb
|
|
103
104
|
- lib/reference_extractor/internal.rb
|
|
105
|
+
- lib/reference_extractor/internal/association_inspector.rb
|
|
104
106
|
- lib/reference_extractor/internal/ast_reference_extractor.rb
|
|
105
107
|
- lib/reference_extractor/internal/const_node_inspector.rb
|
|
106
108
|
- lib/reference_extractor/internal/constant_discovery.rb
|
|
@@ -138,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
138
140
|
- !ruby/object:Gem::Version
|
|
139
141
|
version: '0'
|
|
140
142
|
requirements: []
|
|
141
|
-
rubygems_version:
|
|
143
|
+
rubygems_version: 4.0.3
|
|
142
144
|
specification_version: 4
|
|
143
145
|
summary: Extracts cross-file references in Ruby projects for architecture and dependency
|
|
144
146
|
analysis.
|