referral 0.0.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.
@@ -0,0 +1,67 @@
1
+ require "time"
2
+ require "referral/file_store"
3
+ require "referral/git_store"
4
+
5
+ module Referral
6
+ COLUMN_FUNCTIONS = {
7
+ id: ->(token) {
8
+ token.id
9
+ },
10
+ location: ->(token) {
11
+ "#{token.file}:#{token.line}:#{token.column}:"
12
+ },
13
+ file: ->(token) {
14
+ token.file
15
+ },
16
+ line: ->(token) {
17
+ token.line
18
+ },
19
+ column: ->(token) {
20
+ token.column
21
+ },
22
+ type: ->(token) {
23
+ token.type_name
24
+ },
25
+ scope: ->(token) {
26
+ token.scope
27
+ },
28
+ name: ->(token) {
29
+ token.literal_name
30
+ },
31
+ full_name: ->(token) {
32
+ token.full_name
33
+ },
34
+ source: ->(token) {
35
+ FileStore.read_line(token.file, token.line)
36
+ },
37
+ git_sha: ->(token) {
38
+ GitStore.sha(token.file, token.line)
39
+ },
40
+ git_author: ->(token) {
41
+ GitStore.author(token.file, token.line)
42
+ },
43
+ git_commit_at: ->(token) {
44
+ GitStore.time(token.file, token.line)&.utc&.iso8601
45
+ },
46
+
47
+ }
48
+
49
+ class PrintsResults
50
+ def call(result, options)
51
+ if options[:print_headers]
52
+ puts options[:columns].join(options[:delimiter])
53
+ end
54
+
55
+ result.tokens.each do |token|
56
+ cells = options[:columns].map { |column_name|
57
+ if (column = COLUMN_FUNCTIONS[column_name.to_sym])
58
+ column.call(token)
59
+ else
60
+ raise Referral::Error.new("Column '#{column_name}' not found in Referral::COLUMN_FUNCTIONS")
61
+ end
62
+ }
63
+ puts cells.join(options[:delimiter])
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,20 @@
1
+ require "referral/filters_tokens"
2
+ require "referral/sorts_tokens"
3
+ require "referral/scans_tokens"
4
+ require "referral/value/result"
5
+
6
+ module Referral
7
+ class Runner
8
+ def call(options)
9
+ Value::Result.new(
10
+ tokens: SortsTokens.new.call(
11
+ FiltersTokens.new.call(
12
+ ScansTokens.new.call(files: options.files),
13
+ options
14
+ ),
15
+ options
16
+ )
17
+ )
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ require "referral/translates_node_to_token"
2
+ require "referral/expands_directories"
3
+ require "referral/tokenizes_identifiers"
4
+
5
+ module Referral
6
+ class ScansTokens
7
+ def initialize
8
+ @expands_directories = ExpandsDirectories.new
9
+ @translates_node_to_token = TranslatesNodeToToken.new
10
+ @tokenizes_identifiers = TokenizesIdentifiers.new
11
+ end
12
+
13
+ def call(files:, &blk)
14
+ @expands_directories.call(files).flat_map { |file|
15
+ begin
16
+ root = RubyVM::AbstractSyntaxTree.parse_file(file)
17
+ find_tokens([root], nil, file)
18
+ rescue SyntaxError => e
19
+ warn "ERROR: Failed to parse \"#{file}\": #{e.message} (#{e.class})"
20
+ rescue SystemCallError => e
21
+ warn "ERROR: Failed to read \"#{file}\": #{e.message} (#{e.class})"
22
+ end
23
+ }.compact
24
+ end
25
+
26
+ private
27
+
28
+ def find_tokens(nodes, parent, file)
29
+ nodes.flat_map { |node|
30
+ next unless node.is_a?(RubyVM::AbstractSyntaxTree::Node)
31
+
32
+ if (token = @translates_node_to_token.call(node, parent, file))
33
+ @tokenizes_identifiers.call(node, token)
34
+ [token, *find_tokens(node.children[1..-1], token, file)]
35
+ else
36
+ find_tokens(node.children, parent, file)
37
+ end
38
+ }.compact
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,46 @@
1
+ require "referral/git_store"
2
+
3
+ module Referral
4
+ SORT_FUNCTIONS = {
5
+ file: ->(tokens) {
6
+ tokens.sort_by { |token|
7
+ [token.file, token.line, token.column, token.id]
8
+ }
9
+ },
10
+ scope: ->(tokens) {
11
+ max_length = tokens.map { |t| t.fully_qualified.size }.max
12
+ tokens.sort_by { |token|
13
+ names = token.fully_qualified.map { |fq| fq.name.to_s }
14
+ [
15
+ *names.fill("", names.size...max_length),
16
+ token.file, token.line, token.column, token.id,
17
+ ]
18
+ }
19
+ },
20
+ least_recent_commit: ->(tokens) {
21
+ tokens.sort_by do |token|
22
+ [
23
+ GitStore.time(token.file, token.line).to_i,
24
+ token.file, token.line, token.column, token.id,
25
+ ]
26
+ end
27
+ },
28
+ most_recent_commit: ->(tokens) {
29
+ tokens.sort_by { |token|
30
+ [
31
+ -1 * GitStore.time(token.file, token.line).to_i,
32
+ token.file, token.line, token.column, token.id,
33
+ ]
34
+ }
35
+ },
36
+ }
37
+ class SortsTokens
38
+ def call(tokens, options)
39
+ if (sort_func = SORT_FUNCTIONS[options[:sort].to_sym])
40
+ sort_func.call(tokens)
41
+ else
42
+ raise Referral::Error.new("Sort '#{options[:sort]} not found in Referral::SORT_FUNCTIONS")
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,186 @@
1
+ require "referral/value/node_type"
2
+
3
+ module Referral
4
+ JOIN_SEPARATORS = {
5
+ double_colon: "::",
6
+ dot: ".",
7
+ hash: "#",
8
+ tilde: "~",
9
+ }
10
+ TOKEN_TYPES = {
11
+ module: Value::NodeType.new(
12
+ name: :module,
13
+ ast_type: :MODULE,
14
+ join_separator: JOIN_SEPARATORS[:double_colon],
15
+ token_type: :definition,
16
+ reverse_identifiers: false,
17
+ good_parent: true,
18
+ name_finder: ->(node) { nil }
19
+ ),
20
+ class: Value::NodeType.new(
21
+ name: :class,
22
+ ast_type: :CLASS,
23
+ join_separator: JOIN_SEPARATORS[:double_colon],
24
+ token_type: :definition,
25
+ reverse_identifiers: false,
26
+ good_parent: true,
27
+ name_finder: ->(node) { nil }
28
+ ),
29
+ constant_def: Value::NodeType.new(
30
+ name: :constant_declaration,
31
+ ast_type: :CDECL,
32
+ join_separator: JOIN_SEPARATORS[:double_colon],
33
+ token_type: :definition,
34
+ reverse_identifiers: false,
35
+ good_parent: true,
36
+ name_finder: ->(node) {
37
+ possible_name = node.children[0]
38
+ possible_name.is_a?(Symbol) ? possible_name : nil
39
+ }
40
+ ),
41
+ class_method: Value::NodeType.new(
42
+ name: :class_method,
43
+ ast_type: :DEFS,
44
+ join_separator: JOIN_SEPARATORS[:dot],
45
+ token_type: :definition,
46
+ reverse_identifiers: false,
47
+ good_parent: true,
48
+ name_finder: ->(node) { node.children[1] }
49
+ ),
50
+ instance_method: Value::NodeType.new(
51
+ name: :instance_method,
52
+ ast_type: :DEFN,
53
+ join_separator: JOIN_SEPARATORS[:hash],
54
+ token_type: :definition,
55
+ reverse_identifiers: false,
56
+ good_parent: true,
57
+ name_finder: ->(node) { node.children[0] }
58
+ ),
59
+ local_var_assign: Value::NodeType.new(
60
+ name: :local_var_assign,
61
+ ast_type: :LASGN,
62
+ join_separator: JOIN_SEPARATORS[:tilde],
63
+ token_type: :definition,
64
+ reverse_identifiers: false,
65
+ good_parent: false,
66
+ name_finder: ->(node) { node.children[0] }
67
+ ),
68
+ instance_var_assign: Value::NodeType.new(
69
+ name: :instance_var_assign,
70
+ ast_type: :IASGN,
71
+ join_separator: JOIN_SEPARATORS[:tilde],
72
+ token_type: :definition,
73
+ reverse_identifiers: false,
74
+ good_parent: false,
75
+ name_finder: ->(node) { node.children[0] }
76
+ ),
77
+ class_var_assign: Value::NodeType.new(
78
+ name: :class_var_assign,
79
+ ast_type: :CVASGN,
80
+ join_separator: JOIN_SEPARATORS[:tilde],
81
+ token_type: :definition,
82
+ reverse_identifiers: false,
83
+ good_parent: false,
84
+ name_finder: ->(node) { node.children[0] }
85
+ ),
86
+ global_var_assign: Value::NodeType.new(
87
+ name: :global_var_assign,
88
+ ast_type: :GASGN,
89
+ join_separator: JOIN_SEPARATORS[:tilde],
90
+ token_type: :definition,
91
+ reverse_identifiers: false,
92
+ good_parent: false,
93
+ name_finder: ->(node) { node.children[0] }
94
+ ),
95
+ attr_assign: Value::NodeType.new(
96
+ name: :attr_assign,
97
+ ast_type: :ATTRASGN,
98
+ join_separator: JOIN_SEPARATORS[:dot],
99
+ token_type: :definition,
100
+ reverse_identifiers: false,
101
+ good_parent: false,
102
+ name_finder: ->(node) { node.children[1] }
103
+ ),
104
+ call: Value::NodeType.new(
105
+ name: :call,
106
+ ast_type: :CALL,
107
+ join_separator: JOIN_SEPARATORS[:dot],
108
+ token_type: :reference,
109
+ reverse_identifiers: true,
110
+ good_parent: true,
111
+ name_finder: ->(node) { node.children[1] }
112
+ ),
113
+ function_call: Value::NodeType.new(
114
+ name: :function_call,
115
+ ast_type: :FCALL,
116
+ join_separator: JOIN_SEPARATORS[:dot],
117
+ token_type: :reference,
118
+ reverse_identifiers: true,
119
+ good_parent: false,
120
+ name_finder: ->(node) { node.children[0] }
121
+ ),
122
+ local_var: Value::NodeType.new(
123
+ name: :local_var,
124
+ ast_type: :LVAR,
125
+ join_separator: JOIN_SEPARATORS[:tilde],
126
+ token_type: :reference,
127
+ reverse_identifiers: true,
128
+ good_parent: false,
129
+ name_finder: ->(node) { node.children[0] }
130
+ ),
131
+ instance_var: Value::NodeType.new(
132
+ name: :instance_var,
133
+ ast_type: :IVAR,
134
+ join_separator: JOIN_SEPARATORS[:tilde],
135
+ token_type: :reference,
136
+ reverse_identifiers: true,
137
+ good_parent: false,
138
+ name_finder: ->(node) { node.children[0] }
139
+ ),
140
+ class_var: Value::NodeType.new(
141
+ name: :class_var,
142
+ ast_type: :CVAR,
143
+ join_separator: JOIN_SEPARATORS[:tilde],
144
+ token_type: :reference,
145
+ reverse_identifiers: true,
146
+ good_parent: false,
147
+ name_finder: ->(node) { node.children[0] }
148
+ ),
149
+ global_var: Value::NodeType.new(
150
+ name: :global_var,
151
+ ast_type: :GVAR,
152
+ join_separator: JOIN_SEPARATORS[:tilde],
153
+ token_type: :reference,
154
+ reverse_identifiers: true,
155
+ good_parent: false,
156
+ name_finder: ->(node) { node.children[0] }
157
+ ),
158
+ constant_ref: Value::NodeType.new(
159
+ name: :constant,
160
+ ast_type: :CONST,
161
+ join_separator: JOIN_SEPARATORS[:double_colon],
162
+ token_type: :reference,
163
+ reverse_identifiers: true,
164
+ good_parent: false,
165
+ name_finder: ->(node) { node.children[0] }
166
+ ),
167
+ double_colon: Value::NodeType.new(
168
+ name: :constant,
169
+ ast_type: :COLON2,
170
+ join_separator: JOIN_SEPARATORS[:double_colon],
171
+ token_type: :reference,
172
+ reverse_identifiers: true,
173
+ good_parent: false,
174
+ name_finder: ->(node) { node.children[1] }
175
+ ),
176
+ triple_colon: Value::NodeType.new(
177
+ name: :constant,
178
+ ast_type: :COLON3,
179
+ join_separator: JOIN_SEPARATORS[:double_colon],
180
+ token_type: :reference,
181
+ reverse_identifiers: true,
182
+ good_parent: false,
183
+ name_finder: ->(node) { node.children[0] }
184
+ ),
185
+ }
186
+ end
@@ -0,0 +1,32 @@
1
+ require "referral/translates_node_to_token"
2
+
3
+ module Referral
4
+ class TokenizesIdentifiers
5
+ def initialize
6
+ @translates_node_to_token = TranslatesNodeToToken.new
7
+ end
8
+
9
+ def call(root_node, root_token)
10
+ find_names(root_node, root_token)
11
+ .reject { |identifier_token| identifier_token.name.nil? }
12
+ .tap do |identifiers|
13
+ root_token.identifiers = identifiers
14
+
15
+ if identifiers.any? { |id| id.node_type == TOKEN_TYPES[:triple_colon] }
16
+ root_token.parent = nil
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def find_names(node, parent)
24
+ return [] unless node.is_a?(RubyVM::AbstractSyntaxTree::Node)
25
+
26
+ [
27
+ *find_names(node.children.first, parent),
28
+ @translates_node_to_token.call(node, parent, parent.file),
29
+ ].compact
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ require "referral/token_types"
2
+ require "referral/value/token"
3
+
4
+ module Referral
5
+ class TranslatesNodeToToken
6
+ def call(node, parent, file)
7
+ return unless (type = TOKEN_TYPES.values.find { |d| node.type == d.ast_type })
8
+
9
+ Value::Token.new(
10
+ name: type.name_finder.call(node),
11
+ node_type: type,
12
+ parent: parent_unless_bad_parent(parent, type),
13
+ file: file,
14
+ line: node.first_lineno,
15
+ column: node.first_column
16
+ )
17
+ end
18
+
19
+ private
20
+
21
+ def parent_unless_bad_parent(parent_token, type)
22
+ return parent_token if parent_token&.node_type&.good_parent
23
+
24
+ # puts "should a #{parent_token.node_type.name} parent a #{type.name}?"
25
+ # return if parent_token.node_type.name == :local_var_assign
26
+ # parent_token
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ module Referral
2
+ module Value
3
+ class NodeType < Struct.new(
4
+ :name,
5
+ :ast_type,
6
+ :join_separator,
7
+ :name_finder,
8
+ :token_type,
9
+ :reverse_identifiers,
10
+ :good_parent,
11
+ keyword_init: true
12
+ )
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,38 @@
1
+ module Referral
2
+ module Value
3
+ class Options < Struct.new(
4
+ :files,
5
+ # Filtering
6
+ :name,
7
+ :exact_name,
8
+ :full_name,
9
+ :pattern,
10
+ :type,
11
+ :include_unnamed,
12
+ # Sorting
13
+ :sort,
14
+ # Printing
15
+ :print_headers,
16
+ :columns,
17
+ :delimiter,
18
+ keyword_init: true
19
+ )
20
+
21
+ def self.default(overrides = {})
22
+ DEFAULT.merge({files: Dir["**/*.rb"]}.merge(overrides))
23
+ end
24
+
25
+ DEFAULT = new(
26
+ columns: ["location", "type", "scope", "name"],
27
+ delimiter: " ",
28
+ include_unnamed: false,
29
+ sort: "file",
30
+ print_headers: false,
31
+ ).freeze
32
+
33
+ def merge(new_opts)
34
+ self.class.new(to_h.merge(new_opts))
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,6 @@
1
+ module Referral
2
+ module Value
3
+ class Result < Struct.new(:tokens, keyword_init: true)
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,68 @@
1
+ require "digest/sha1"
2
+
3
+ module Referral
4
+ module Value
5
+ class Token < Struct.new(
6
+ :name, :identifiers, :node_type, :parent, :file, :line, :column,
7
+ keyword_init: true
8
+ )
9
+
10
+ def fully_qualified
11
+ [
12
+ *parent&.fully_qualified,
13
+ *identity_components,
14
+ ].compact
15
+ end
16
+
17
+ def full_name
18
+ join_names(fully_qualified)
19
+ end
20
+
21
+ def scope
22
+ return "" unless parent
23
+ parent.full_name
24
+ end
25
+
26
+ def literal_name
27
+ if identifiers.empty?
28
+ name.to_s
29
+ else
30
+ join_names(identifiers)
31
+ end
32
+ end
33
+
34
+ def type_name
35
+ node_type.name.to_s
36
+ end
37
+
38
+ def id
39
+ Digest::SHA1.hexdigest(to_h.merge(
40
+ parent: nil,
41
+ identifiers: identifiers&.map(&:id),
42
+ node_type: node_type.name
43
+ ).inspect)[0..6]
44
+ end
45
+
46
+ protected
47
+
48
+ def join_names(tokens)
49
+ tokens.reduce("") { |s, token|
50
+ next s unless token.name
51
+ if s.empty?
52
+ token.name.to_s
53
+ else
54
+ "#{s}#{token.node_type.join_separator}#{token.name}"
55
+ end
56
+ }
57
+ end
58
+
59
+ def identity_components
60
+ if identifiers && !identifiers.empty?
61
+ identifiers
62
+ else
63
+ [self]
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ module Referral
2
+ VERSION = "0.0.1"
3
+ end
data/lib/referral.rb ADDED
@@ -0,0 +1,11 @@
1
+ require "referral/runner"
2
+
3
+ require "referral/version"
4
+ require "referral/error"
5
+ require "referral/cli"
6
+
7
+ module Referral
8
+ def self.run(*args, **kwargs, &blk)
9
+ Runner.new.call(*args, **kwargs, &blk)
10
+ end
11
+ end
data/referral.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ lib = File.expand_path("lib", __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "referral/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "referral"
7
+ spec.version = Referral::VERSION
8
+ spec.authors = ["Justin Searls"]
9
+ spec.email = ["searls@gmail.com"]
10
+
11
+ spec.summary = "Scan for definitions and references in your Ruby code."
12
+ spec.homepage = "https://github.com/testdouble/referral"
13
+ spec.license = "MIT"
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = spec.homepage
17
+
18
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
19
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.17.3"
26
+ spec.add_development_dependency "rake", "~> 12.3"
27
+ spec.add_development_dependency "minitest", "~> 5.0"
28
+ spec.add_development_dependency "standard"
29
+ end