referral 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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