syntax_tree 1.1.1 → 2.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.
- checksums.yaml +4 -4
- data/.github/workflows/gh-pages.yml +26 -0
- data/.github/workflows/main.yml +1 -1
- data/.gitignore +2 -1
- data/CHANGELOG.md +38 -5
- data/Gemfile.lock +8 -7
- data/README.md +6 -6
- data/bin/bench +11 -11
- data/bin/console +3 -3
- data/bin/profile +3 -3
- data/doc/logo.svg +284 -0
- data/exe/stree +1 -0
- data/lib/syntax_tree/cli.rb +86 -25
- data/lib/syntax_tree/formatter.rb +71 -0
- data/lib/syntax_tree/language_server/inlay_hints.rb +93 -0
- data/lib/syntax_tree/language_server.rb +99 -0
- data/lib/syntax_tree/node.rb +11505 -0
- data/lib/syntax_tree/parser.rb +3098 -0
- data/lib/syntax_tree/version.rb +2 -4
- data/lib/syntax_tree.rb +17 -13752
- metadata +10 -3
data/lib/syntax_tree/cli.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
module SyntaxTree
|
4
4
|
module CLI
|
5
5
|
# A utility wrapper around colored strings in the output.
|
6
6
|
class Color
|
@@ -15,6 +15,10 @@ class SyntaxTree
|
|
15
15
|
"\033[#{code}m#{value}\033[0m"
|
16
16
|
end
|
17
17
|
|
18
|
+
def self.bold(value)
|
19
|
+
new(value, "1")
|
20
|
+
end
|
21
|
+
|
18
22
|
def self.gray(value)
|
19
23
|
new(value, "38;5;102")
|
20
24
|
end
|
@@ -30,7 +34,7 @@ class SyntaxTree
|
|
30
34
|
|
31
35
|
# The parent action class for the CLI that implements the basics.
|
32
36
|
class Action
|
33
|
-
def run(filepath, source)
|
37
|
+
def run(handler, filepath, source)
|
34
38
|
end
|
35
39
|
|
36
40
|
def success
|
@@ -42,8 +46,8 @@ class SyntaxTree
|
|
42
46
|
|
43
47
|
# An action of the CLI that prints out the AST for the given source.
|
44
48
|
class AST < Action
|
45
|
-
def run(filepath, source)
|
46
|
-
pp
|
49
|
+
def run(handler, filepath, source)
|
50
|
+
pp handler.parse(source)
|
47
51
|
end
|
48
52
|
end
|
49
53
|
|
@@ -53,8 +57,8 @@ class SyntaxTree
|
|
53
57
|
class UnformattedError < StandardError
|
54
58
|
end
|
55
59
|
|
56
|
-
def run(filepath, source)
|
57
|
-
raise UnformattedError if source !=
|
60
|
+
def run(handler, filepath, source)
|
61
|
+
raise UnformattedError if source != handler.format(source)
|
58
62
|
rescue StandardError
|
59
63
|
warn("[#{Color.yellow("warn")}] #{filepath}")
|
60
64
|
raise
|
@@ -75,11 +79,11 @@ class SyntaxTree
|
|
75
79
|
class NonIdempotentFormatError < StandardError
|
76
80
|
end
|
77
81
|
|
78
|
-
def run(filepath, source)
|
82
|
+
def run(handler, filepath, source)
|
79
83
|
warning = "[#{Color.yellow("warn")}] #{filepath}"
|
80
|
-
formatted =
|
84
|
+
formatted = handler.format(source)
|
81
85
|
|
82
|
-
if formatted !=
|
86
|
+
if formatted != handler.format(formatted)
|
83
87
|
raise NonIdempotentFormatError
|
84
88
|
end
|
85
89
|
rescue StandardError
|
@@ -98,28 +102,28 @@ class SyntaxTree
|
|
98
102
|
|
99
103
|
# An action of the CLI that prints out the doc tree IR for the given source.
|
100
104
|
class Doc < Action
|
101
|
-
def run(filepath, source)
|
105
|
+
def run(handler, filepath, source)
|
102
106
|
formatter = Formatter.new([])
|
103
|
-
|
107
|
+
handler.parse(source).format(formatter)
|
104
108
|
pp formatter.groups.first
|
105
109
|
end
|
106
110
|
end
|
107
111
|
|
108
112
|
# An action of the CLI that formats the input source and prints it out.
|
109
113
|
class Format < Action
|
110
|
-
def run(filepath, source)
|
111
|
-
puts
|
114
|
+
def run(handler, filepath, source)
|
115
|
+
puts handler.format(source)
|
112
116
|
end
|
113
117
|
end
|
114
118
|
|
115
119
|
# An action of the CLI that formats the input source and writes the
|
116
120
|
# formatted output back to the file.
|
117
121
|
class Write < Action
|
118
|
-
def run(filepath, source)
|
122
|
+
def run(handler, filepath, source)
|
119
123
|
print filepath
|
120
124
|
start = Time.now
|
121
125
|
|
122
|
-
formatted =
|
126
|
+
formatted = handler.format(source)
|
123
127
|
File.write(filepath, formatted)
|
124
128
|
|
125
129
|
color = source == formatted ? Color.gray(filepath) : filepath
|
@@ -135,24 +139,65 @@ class SyntaxTree
|
|
135
139
|
# The help message displayed if the input arguments are not correctly
|
136
140
|
# ordered or formatted.
|
137
141
|
HELP = <<~HELP
|
138
|
-
stree
|
142
|
+
#{Color.bold("stree ast [OPTIONS] [FILE]")}
|
143
|
+
Print out the AST corresponding to the given files
|
144
|
+
|
145
|
+
#{Color.bold("stree check [OPTIONS] [FILE]")}
|
146
|
+
Check that the given files are formatted as syntax tree would format them
|
147
|
+
|
148
|
+
#{Color.bold("stree debug [OPTIONS] [FILE]")}
|
149
|
+
Check that the given files can be formatted idempotently
|
150
|
+
|
151
|
+
#{Color.bold("stree doc [OPTIONS] [FILE]")}
|
152
|
+
Print out the doc tree that would be used to format the given files
|
153
|
+
|
154
|
+
#{Color.bold("stree format [OPTIONS] [FILE]")}
|
155
|
+
Print out the formatted version of the given files
|
156
|
+
|
157
|
+
#{Color.bold("stree help")}
|
158
|
+
Display this help message
|
159
|
+
|
160
|
+
#{Color.bold("stree lsp")}
|
161
|
+
Run syntax tree in language server mode
|
139
162
|
|
140
|
-
|
141
|
-
|
163
|
+
#{Color.bold("stree version")}
|
164
|
+
Output the current version of syntax tree
|
165
|
+
|
166
|
+
#{Color.bold("stree write [OPTIONS] [FILE]")}
|
167
|
+
Read, format, and write back the source of the given files
|
168
|
+
|
169
|
+
[OPTIONS]
|
170
|
+
|
171
|
+
--plugins=...
|
172
|
+
A comma-separated list of plugins to load.
|
142
173
|
HELP
|
143
174
|
|
144
175
|
class << self
|
145
176
|
# Run the CLI over the given array of strings that make up the arguments
|
146
177
|
# passed to the invocation.
|
147
178
|
def run(argv)
|
148
|
-
|
179
|
+
name, *arguments = argv
|
180
|
+
|
181
|
+
case name
|
182
|
+
when "help"
|
183
|
+
puts HELP
|
184
|
+
return 0
|
185
|
+
when "lsp"
|
186
|
+
require "syntax_tree/language_server"
|
187
|
+
LanguageServer.new.run
|
188
|
+
return 0
|
189
|
+
when "version"
|
190
|
+
puts SyntaxTree::VERSION
|
191
|
+
return 0
|
192
|
+
end
|
193
|
+
|
194
|
+
if arguments.empty?
|
149
195
|
warn(HELP)
|
150
196
|
return 1
|
151
197
|
end
|
152
198
|
|
153
|
-
arg, *patterns = argv
|
154
199
|
action =
|
155
|
-
case
|
200
|
+
case name
|
156
201
|
when "a", "ast"
|
157
202
|
AST.new
|
158
203
|
when "c", "check"
|
@@ -170,15 +215,31 @@ class SyntaxTree
|
|
170
215
|
return 1
|
171
216
|
end
|
172
217
|
|
218
|
+
# If there are any plugins specified on the command line, then load them
|
219
|
+
# by requiring them here. We do this by transforming something like
|
220
|
+
#
|
221
|
+
# stree format --plugins=haml template.haml
|
222
|
+
#
|
223
|
+
# into
|
224
|
+
#
|
225
|
+
# require "syntax_tree/haml"
|
226
|
+
#
|
227
|
+
if arguments.first.start_with?("--plugins=")
|
228
|
+
plugins = arguments.shift[/^--plugins=(.*)$/, 1]
|
229
|
+
plugins.split(",").each { |plugin| require "syntax_tree/#{plugin}" }
|
230
|
+
end
|
231
|
+
|
173
232
|
errored = false
|
174
|
-
|
233
|
+
arguments.each do |pattern|
|
175
234
|
Dir.glob(pattern).each do |filepath|
|
176
235
|
next unless File.file?(filepath)
|
177
|
-
|
236
|
+
|
237
|
+
handler = HANDLERS[File.extname(filepath)]
|
238
|
+
source = handler.read(filepath)
|
178
239
|
|
179
240
|
begin
|
180
|
-
action.run(filepath, source)
|
181
|
-
rescue ParseError => error
|
241
|
+
action.run(handler, filepath, source)
|
242
|
+
rescue Parser::ParseError => error
|
182
243
|
warn("Error: #{error.message}")
|
183
244
|
|
184
245
|
if error.lineno
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxTree
|
4
|
+
# A slightly enhanced PP that knows how to format recursively including
|
5
|
+
# comments.
|
6
|
+
class Formatter < PP
|
7
|
+
COMMENT_PRIORITY = 1
|
8
|
+
HEREDOC_PRIORITY = 2
|
9
|
+
|
10
|
+
attr_reader :source, :stack, :quote
|
11
|
+
|
12
|
+
def initialize(source, ...)
|
13
|
+
super(...)
|
14
|
+
|
15
|
+
@source = source
|
16
|
+
@stack = []
|
17
|
+
@quote = "\""
|
18
|
+
end
|
19
|
+
|
20
|
+
def format(node, stackable: true)
|
21
|
+
stack << node if stackable
|
22
|
+
doc = nil
|
23
|
+
|
24
|
+
# If there are comments, then we're going to format them around the node
|
25
|
+
# so that they get printed properly.
|
26
|
+
if node.comments.any?
|
27
|
+
leading, trailing = node.comments.partition(&:leading?)
|
28
|
+
|
29
|
+
# Print all comments that were found before the node.
|
30
|
+
leading.each do |comment|
|
31
|
+
comment.format(self)
|
32
|
+
breakable(force: true)
|
33
|
+
end
|
34
|
+
|
35
|
+
# If the node has a stree-ignore comment right before it, then we're
|
36
|
+
# going to just print out the node as it was seen in the source.
|
37
|
+
if leading.last&.ignore?
|
38
|
+
doc = text(source[node.location.start_char...node.location.end_char])
|
39
|
+
else
|
40
|
+
doc = node.format(self)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Print all comments that were found after the node.
|
44
|
+
trailing.each do |comment|
|
45
|
+
line_suffix(priority: COMMENT_PRIORITY) do
|
46
|
+
text(" ")
|
47
|
+
comment.format(self)
|
48
|
+
break_parent
|
49
|
+
end
|
50
|
+
end
|
51
|
+
else
|
52
|
+
doc = node.format(self)
|
53
|
+
end
|
54
|
+
|
55
|
+
stack.pop if stackable
|
56
|
+
doc
|
57
|
+
end
|
58
|
+
|
59
|
+
def format_each(nodes)
|
60
|
+
nodes.each { |node| format(node) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def parent
|
64
|
+
stack[-2]
|
65
|
+
end
|
66
|
+
|
67
|
+
def parents
|
68
|
+
stack[0...-1].reverse_each
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxTree
|
4
|
+
class LanguageServer
|
5
|
+
class InlayHints
|
6
|
+
attr_reader :before, :after
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@before = Hash.new { |hash, key| hash[key] = +"" }
|
10
|
+
@after = Hash.new { |hash, key| hash[key] = +"" }
|
11
|
+
end
|
12
|
+
|
13
|
+
# Adds the implicitly rescued StandardError into a bare rescue clause. For
|
14
|
+
# example,
|
15
|
+
#
|
16
|
+
# begin
|
17
|
+
# rescue
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# becomes
|
21
|
+
#
|
22
|
+
# begin
|
23
|
+
# rescue StandardError
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
def bare_rescue(location)
|
27
|
+
after[location.start_char + "rescue".length] << " StandardError"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Adds the implicitly referenced value (local variable or method call)
|
31
|
+
# that is added into a hash when the value of a key-value pair is omitted.
|
32
|
+
# For example,
|
33
|
+
#
|
34
|
+
# { value: }
|
35
|
+
#
|
36
|
+
# becomes
|
37
|
+
#
|
38
|
+
# { value: value }
|
39
|
+
#
|
40
|
+
def missing_hash_value(key, location)
|
41
|
+
after[location.end_char] << " #{key}"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Adds implicit parentheses around certain expressions to make it clear
|
45
|
+
# which subexpression will be evaluated first. For example,
|
46
|
+
#
|
47
|
+
# a + b * c
|
48
|
+
#
|
49
|
+
# becomes
|
50
|
+
#
|
51
|
+
# a + ₍b * c₎
|
52
|
+
#
|
53
|
+
def precedence_parentheses(location)
|
54
|
+
before[location.start_char] << "₍"
|
55
|
+
after[location.end_char] << "₎"
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.find(program)
|
59
|
+
inlay_hints = new
|
60
|
+
queue = [[nil, program]]
|
61
|
+
|
62
|
+
until queue.empty?
|
63
|
+
parent_node, child_node = queue.shift
|
64
|
+
|
65
|
+
child_node.child_nodes.each do |grand_child_node|
|
66
|
+
queue << [child_node, grand_child_node] if grand_child_node
|
67
|
+
end
|
68
|
+
|
69
|
+
case [parent_node, child_node]
|
70
|
+
in _, Rescue[exception: nil, location:]
|
71
|
+
inlay_hints.bare_rescue(location)
|
72
|
+
in _, Assoc[key: Label[value: key], value: nil, location:]
|
73
|
+
inlay_hints.missing_hash_value(key[0...-1], location)
|
74
|
+
in Assign | Binary | IfOp | OpAssign, IfOp[location:]
|
75
|
+
inlay_hints.precedence_parentheses(location)
|
76
|
+
in Assign | OpAssign, Binary[location:]
|
77
|
+
inlay_hints.precedence_parentheses(location)
|
78
|
+
in Binary[operator: parent_oper], Binary[operator: child_oper, location:] if parent_oper != child_oper
|
79
|
+
inlay_hints.precedence_parentheses(location)
|
80
|
+
in Binary, Unary[operator: "-", location:]
|
81
|
+
inlay_hints.precedence_parentheses(location)
|
82
|
+
in Params, Assign[location:]
|
83
|
+
inlay_hints.precedence_parentheses(location)
|
84
|
+
else
|
85
|
+
# do nothing
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
inlay_hints
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cgi"
|
4
|
+
require "json"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
require_relative "language_server/inlay_hints"
|
8
|
+
|
9
|
+
module SyntaxTree
|
10
|
+
class LanguageServer
|
11
|
+
attr_reader :input, :output
|
12
|
+
|
13
|
+
def initialize(input: STDIN, output: STDOUT)
|
14
|
+
@input = input.binmode
|
15
|
+
@output = output.binmode
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
store =
|
20
|
+
Hash.new do |hash, uri|
|
21
|
+
hash[uri] = File.binread(CGI.unescape(URI.parse(uri).path))
|
22
|
+
end
|
23
|
+
|
24
|
+
while headers = input.gets("\r\n\r\n")
|
25
|
+
source = input.read(headers[/Content-Length: (\d+)/i, 1].to_i)
|
26
|
+
request = JSON.parse(source, symbolize_names: true)
|
27
|
+
|
28
|
+
case request
|
29
|
+
in { method: "initialize", id: }
|
30
|
+
store.clear
|
31
|
+
write(id: id, result: { capabilities: capabilities })
|
32
|
+
in { method: "initialized" }
|
33
|
+
# ignored
|
34
|
+
in { method: "shutdown" }
|
35
|
+
store.clear
|
36
|
+
return
|
37
|
+
in { method: "textDocument/didChange", params: { textDocument: { uri: }, contentChanges: [{ text: }, *] } }
|
38
|
+
store[uri] = text
|
39
|
+
in { method: "textDocument/didOpen", params: { textDocument: { uri:, text: } } }
|
40
|
+
store[uri] = text
|
41
|
+
in { method: "textDocument/didClose", params: { textDocument: { uri: } } }
|
42
|
+
store.delete(uri)
|
43
|
+
in { method: "textDocument/formatting", id:, params: { textDocument: { uri: } } }
|
44
|
+
write(id: id, result: [format(store[uri])])
|
45
|
+
in { method: "textDocument/inlayHints", id:, params: { textDocument: { uri: } } }
|
46
|
+
write(id: id, result: inlay_hints(store[uri]))
|
47
|
+
in { method: "syntaxTree/visualizing", id:, params: { textDocument: { uri: } } }
|
48
|
+
output = []
|
49
|
+
PP.pp(SyntaxTree.parse(store[uri]), output)
|
50
|
+
write(id: id, result: output.join)
|
51
|
+
in { method: %r{\$/.+} }
|
52
|
+
# ignored
|
53
|
+
else
|
54
|
+
raise "Unhandled: #{request}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def capabilities
|
62
|
+
{
|
63
|
+
documentFormattingProvider: true,
|
64
|
+
textDocumentSync: { change: 1, openClose: true }
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def format(source)
|
69
|
+
{
|
70
|
+
range: {
|
71
|
+
start: { line: 0, character: 0 },
|
72
|
+
end: { line: source.lines.size + 1, character: 0 }
|
73
|
+
},
|
74
|
+
newText: SyntaxTree.format(source)
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
def log(message)
|
79
|
+
write(method: "window/logMessage", params: { type: 4, message: message })
|
80
|
+
end
|
81
|
+
|
82
|
+
def inlay_hints(source)
|
83
|
+
inlay_hints = InlayHints.find(SyntaxTree.parse(source))
|
84
|
+
serialize = ->(position, text) { { position: position, text: text } }
|
85
|
+
|
86
|
+
{
|
87
|
+
before: inlay_hints.before.map(&serialize),
|
88
|
+
after: inlay_hints.after.map(&serialize)
|
89
|
+
}
|
90
|
+
rescue Parser::ParseError
|
91
|
+
end
|
92
|
+
|
93
|
+
def write(value)
|
94
|
+
response = value.merge(jsonrpc: "2.0").to_json
|
95
|
+
output.print("Content-Length: #{response.bytesize}\r\n\r\n#{response}")
|
96
|
+
output.flush
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|