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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bac9513331865230939596e19a86fb7e446498d4c4f9194dcb68a97b5377a668
4
- data.tar.gz: e5db87762acef10e792fb4f56b6cd333ea8f08a43b2f40f9d7c9f5206eb7daa0
3
+ metadata.gz: 19d808e45b0a49435136cb0813dbc66af70b68f1127c1d40d497b97f77da470c
4
+ data.tar.gz: 9f7419a764a29617ed8d8e33c3783c527a2854e697c8439a83139dbff16268d7
5
5
  SHA512:
6
- metadata.gz: 5439eaacdaf0df2b261122de2d5853542b4a51841945197aca3ee04b33dc259348946401598125faea3c7f941a826f06d1ef5dd265ee3a56dece72e944d0a6c8
7
- data.tar.gz: 5970480b43f52c8abfe2c4d0a54dfffca9b024113d19a2642b6f93b10e4b5cfbf540dec4b0887ec0380d500846032358f889f96c1cfd70c0399172c57a562ce3
6
+ metadata.gz: 7adb979d6be9eee24fc653046c46bfd243f4c762e4afc27b639a9b477ea7fa555662f8a3f649300a51294bdd193f6b83959e53877f3ab03bf8ad2d657648f177
7
+ data.tar.gz: 872d6e9a503e1027bb976a6ec600fc2f1b272f73d5e9fc1fc1dabe8d5300fa59b469fc8562f94904873196fa81b4b6f4d084f33f08108ec687e903b903a58060
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.4.7
1
+ ruby 4.0.1
data/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.1.1] - 2026-01-24
6
+
7
+ - added association inspector from Packwerk
8
+ - better tests, docs, and some refactoring
9
+
5
10
  ## [0.1.0] - 2025-10-30
6
11
 
7
12
  - Initial release
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).to_s
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
- # TO DO: Add association inspector
70
- constant_name_inspectors: [Internal::ConstNodeInspector.new],
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.to_s == unresolved_reference.relative_path.to_s
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
@@ -4,6 +4,7 @@ module ReferenceExtractor
4
4
  module Internal
5
5
  extend ActiveSupport::Autoload
6
6
 
7
+ autoload :AssociationInspector
7
8
  autoload :AstReferenceExtractor
8
9
  autoload :ConstNodeInspector
9
10
  autoload :ConstantDiscovery
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReferenceExtractor
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
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.0
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: 3.7.2
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.