syntax_tree 2.4.1 → 2.7.0
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/main.yml +1 -1
- data/CHANGELOG.md +37 -1
- data/Gemfile +0 -2
- data/Gemfile.lock +5 -3
- data/README.md +63 -2
- data/Rakefile +5 -20
- data/lib/syntax_tree/basic_visitor.rb +74 -0
- data/lib/syntax_tree/formatter/trailing_comma.rb +13 -0
- data/lib/syntax_tree/formatter.rb +8 -2
- data/lib/syntax_tree/node.rb +174 -118
- data/lib/syntax_tree/parser.rb +112 -0
- data/lib/syntax_tree/plugin/trailing_comma.rb +4 -0
- data/lib/syntax_tree/rake/check_task.rb +66 -0
- data/lib/syntax_tree/rake/write_task.rb +66 -0
- data/lib/syntax_tree/rake_tasks.rb +4 -0
- data/lib/syntax_tree/version.rb +1 -1
- data/lib/syntax_tree/visitor/field_visitor.rb +9 -3
- data/lib/syntax_tree/visitor/match_visitor.rb +2 -2
- data/lib/syntax_tree/visitor.rb +4 -66
- data/lib/syntax_tree.rb +5 -27
- data/syntax_tree.gemspec +3 -0
- metadata +36 -3
- data/lib/syntax_tree/prettyprint.rb +0 -1159
data/lib/syntax_tree/node.rb
CHANGED
@@ -123,7 +123,7 @@ module SyntaxTree
|
|
123
123
|
end
|
124
124
|
|
125
125
|
def construct_keys
|
126
|
-
|
126
|
+
PrettierPrint.format(+"") { |q| Visitor::MatchVisitor.new(q).visit(self) }
|
127
127
|
end
|
128
128
|
end
|
129
129
|
|
@@ -597,10 +597,30 @@ module SyntaxTree
|
|
597
597
|
q.indent do
|
598
598
|
q.breakable("")
|
599
599
|
q.format(arguments)
|
600
|
+
q.if_break { q.text(",") } if q.trailing_comma? && trailing_comma?
|
600
601
|
end
|
601
602
|
q.breakable("")
|
602
603
|
end
|
603
604
|
end
|
605
|
+
|
606
|
+
private
|
607
|
+
|
608
|
+
def trailing_comma?
|
609
|
+
case arguments
|
610
|
+
in Args[parts: [*, ArgBlock]]
|
611
|
+
# If the last argument is a block, then we can't put a trailing comma
|
612
|
+
# after it without resulting in a syntax error.
|
613
|
+
false
|
614
|
+
in Args[parts: [Command | CommandCall]]
|
615
|
+
# If the only argument is a command or command call, then a trailing
|
616
|
+
# comma would be parsed as part of that expression instead of on this
|
617
|
+
# one, so we don't want to add a trailing comma.
|
618
|
+
false
|
619
|
+
else
|
620
|
+
# Otherwise, we should be okay to add a trailing comma.
|
621
|
+
true
|
622
|
+
end
|
623
|
+
end
|
604
624
|
end
|
605
625
|
|
606
626
|
# Args represents a list of arguments being passed to a method call or array
|
@@ -859,6 +879,7 @@ module SyntaxTree
|
|
859
879
|
end
|
860
880
|
|
861
881
|
q.seplist(contents.parts, separator) { |part| q.format(part) }
|
882
|
+
q.if_break { q.text(",") } if q.trailing_comma?
|
862
883
|
end
|
863
884
|
q.breakable("")
|
864
885
|
end
|
@@ -954,6 +975,7 @@ module SyntaxTree
|
|
954
975
|
q.indent do
|
955
976
|
q.breakable("")
|
956
977
|
q.format(contents)
|
978
|
+
q.if_break { q.text(",") } if q.trailing_comma?
|
957
979
|
end
|
958
980
|
end
|
959
981
|
|
@@ -1109,7 +1131,11 @@ module SyntaxTree
|
|
1109
1131
|
q.group do
|
1110
1132
|
q.format(constant)
|
1111
1133
|
q.text("[")
|
1112
|
-
q.
|
1134
|
+
q.indent do
|
1135
|
+
q.breakable("")
|
1136
|
+
q.seplist(parts) { |part| q.format(part) }
|
1137
|
+
end
|
1138
|
+
q.breakable("")
|
1113
1139
|
q.text("]")
|
1114
1140
|
end
|
1115
1141
|
|
@@ -1119,7 +1145,11 @@ module SyntaxTree
|
|
1119
1145
|
parent = q.parent
|
1120
1146
|
if parts.length == 1 || PATTERNS.include?(parent.class)
|
1121
1147
|
q.text("[")
|
1122
|
-
q.
|
1148
|
+
q.indent do
|
1149
|
+
q.breakable("")
|
1150
|
+
q.seplist(parts) { |part| q.format(part) }
|
1151
|
+
end
|
1152
|
+
q.breakable("")
|
1123
1153
|
q.text("]")
|
1124
1154
|
elsif parts.empty?
|
1125
1155
|
q.text("[]")
|
@@ -1388,7 +1418,7 @@ module SyntaxTree
|
|
1388
1418
|
module HashKeyFormatter
|
1389
1419
|
# Formats the keys of a hash literal using labels.
|
1390
1420
|
class Labels
|
1391
|
-
LABEL =
|
1421
|
+
LABEL = /\A[A-Za-z_](\w*[\w!?])?\z/
|
1392
1422
|
|
1393
1423
|
def format_key(q, key)
|
1394
1424
|
case key
|
@@ -1666,52 +1696,6 @@ module SyntaxTree
|
|
1666
1696
|
end
|
1667
1697
|
end
|
1668
1698
|
|
1669
|
-
# This module will remove any breakables from the list of contents so that no
|
1670
|
-
# newlines are present in the output.
|
1671
|
-
module RemoveBreaks
|
1672
|
-
class << self
|
1673
|
-
def call(doc)
|
1674
|
-
marker = Object.new
|
1675
|
-
stack = [doc]
|
1676
|
-
|
1677
|
-
while stack.any?
|
1678
|
-
doc = stack.pop
|
1679
|
-
|
1680
|
-
if doc == marker
|
1681
|
-
stack.pop
|
1682
|
-
next
|
1683
|
-
end
|
1684
|
-
|
1685
|
-
stack += [doc, marker]
|
1686
|
-
|
1687
|
-
case doc
|
1688
|
-
when PrettyPrint::Align, PrettyPrint::Indent, PrettyPrint::Group
|
1689
|
-
doc.contents.map! { |child| remove_breaks(child) }
|
1690
|
-
stack += doc.contents.reverse
|
1691
|
-
when PrettyPrint::IfBreak
|
1692
|
-
doc.flat_contents.map! { |child| remove_breaks(child) }
|
1693
|
-
stack += doc.flat_contents.reverse
|
1694
|
-
end
|
1695
|
-
end
|
1696
|
-
end
|
1697
|
-
|
1698
|
-
private
|
1699
|
-
|
1700
|
-
def remove_breaks(doc)
|
1701
|
-
case doc
|
1702
|
-
when PrettyPrint::Breakable
|
1703
|
-
text = PrettyPrint::Text.new
|
1704
|
-
text.add(object: doc.force? ? "; " : doc.separator, width: doc.width)
|
1705
|
-
text
|
1706
|
-
when PrettyPrint::IfBreak
|
1707
|
-
PrettyPrint::Align.new(indent: 0, contents: doc.flat_contents)
|
1708
|
-
else
|
1709
|
-
doc
|
1710
|
-
end
|
1711
|
-
end
|
1712
|
-
end
|
1713
|
-
end
|
1714
|
-
|
1715
1699
|
# BlockVar represents the parameters being declared for a block. Effectively
|
1716
1700
|
# this node is everything contained within the pipes. This includes all of the
|
1717
1701
|
# various parameter types, as well as block-local variable declarations.
|
@@ -1752,8 +1736,7 @@ module SyntaxTree
|
|
1752
1736
|
|
1753
1737
|
def format(q)
|
1754
1738
|
q.group(0, "|", "|") do
|
1755
|
-
|
1756
|
-
RemoveBreaks.call(doc)
|
1739
|
+
q.remove_breaks(q.format(params))
|
1757
1740
|
|
1758
1741
|
if locals.any?
|
1759
1742
|
q.text("; ")
|
@@ -2802,10 +2785,17 @@ module SyntaxTree
|
|
2802
2785
|
q.format(value)
|
2803
2786
|
q.text(" ")
|
2804
2787
|
q.format(operator)
|
2805
|
-
|
2806
|
-
|
2807
|
-
|
2808
|
-
|
2788
|
+
|
2789
|
+
case pattern
|
2790
|
+
in AryPtn | FndPtn | HshPtn
|
2791
|
+
q.text(" ")
|
2792
|
+
q.format(pattern)
|
2793
|
+
else
|
2794
|
+
q.group do
|
2795
|
+
q.indent do
|
2796
|
+
q.breakable
|
2797
|
+
q.format(pattern)
|
2798
|
+
end
|
2809
2799
|
end
|
2810
2800
|
end
|
2811
2801
|
end
|
@@ -3077,8 +3067,20 @@ module SyntaxTree
|
|
3077
3067
|
doc =
|
3078
3068
|
q.nest(0) do
|
3079
3069
|
q.format(receiver)
|
3080
|
-
|
3081
|
-
|
3070
|
+
|
3071
|
+
# If there are leading comments on the message then we know we have
|
3072
|
+
# a newline in the source that is forcing these things apart. In
|
3073
|
+
# this case we will have to use a trailing operator.
|
3074
|
+
if message.comments.any?(&:leading?)
|
3075
|
+
q.format(CallOperatorFormatter.new(operator), stackable: false)
|
3076
|
+
q.indent do
|
3077
|
+
q.breakable("")
|
3078
|
+
q.format(message)
|
3079
|
+
end
|
3080
|
+
else
|
3081
|
+
q.format(CallOperatorFormatter.new(operator), stackable: false)
|
3082
|
+
q.format(message)
|
3083
|
+
end
|
3082
3084
|
end
|
3083
3085
|
|
3084
3086
|
case arguments
|
@@ -3096,31 +3098,6 @@ module SyntaxTree
|
|
3096
3098
|
|
3097
3099
|
private
|
3098
3100
|
|
3099
|
-
# This is a somewhat naive method that is attempting to sum up the width of
|
3100
|
-
# the doc nodes that make up the given doc node. This is used to align
|
3101
|
-
# content.
|
3102
|
-
def doc_width(parent)
|
3103
|
-
queue = [parent]
|
3104
|
-
width = 0
|
3105
|
-
|
3106
|
-
until queue.empty?
|
3107
|
-
doc = queue.shift
|
3108
|
-
|
3109
|
-
case doc
|
3110
|
-
when PrettyPrint::Text
|
3111
|
-
width += doc.width
|
3112
|
-
when PrettyPrint::Indent, PrettyPrint::Align, PrettyPrint::Group
|
3113
|
-
queue = doc.contents + queue
|
3114
|
-
when PrettyPrint::IfBreak
|
3115
|
-
queue = doc.break_contents + queue
|
3116
|
-
when PrettyPrint::Breakable
|
3117
|
-
width = 0
|
3118
|
-
end
|
3119
|
-
end
|
3120
|
-
|
3121
|
-
width
|
3122
|
-
end
|
3123
|
-
|
3124
3101
|
def argument_alignment(q, doc)
|
3125
3102
|
# Very special handling case for rspec matchers. In general with rspec
|
3126
3103
|
# matchers you expect to see something like:
|
@@ -3138,7 +3115,7 @@ module SyntaxTree
|
|
3138
3115
|
if %w[to not_to to_not].include?(message.value)
|
3139
3116
|
0
|
3140
3117
|
else
|
3141
|
-
width =
|
3118
|
+
width = q.last_position(doc) + 1
|
3142
3119
|
width > (q.maxwidth / 2) ? 0 : width
|
3143
3120
|
end
|
3144
3121
|
end
|
@@ -4611,16 +4588,26 @@ module SyntaxTree
|
|
4611
4588
|
|
4612
4589
|
def format(q)
|
4613
4590
|
q.format(constant) if constant
|
4614
|
-
q.group(0, "[", "]") do
|
4615
|
-
q.text("*")
|
4616
|
-
q.format(left)
|
4617
|
-
q.comma_breakable
|
4618
4591
|
|
4619
|
-
|
4620
|
-
q.
|
4592
|
+
q.group do
|
4593
|
+
q.text("[")
|
4594
|
+
|
4595
|
+
q.indent do
|
4596
|
+
q.breakable("")
|
4621
4597
|
|
4622
|
-
|
4623
|
-
|
4598
|
+
q.text("*")
|
4599
|
+
q.format(left)
|
4600
|
+
q.comma_breakable
|
4601
|
+
|
4602
|
+
q.seplist(values) { |value| q.format(value) }
|
4603
|
+
q.comma_breakable
|
4604
|
+
|
4605
|
+
q.text("*")
|
4606
|
+
q.format(right)
|
4607
|
+
end
|
4608
|
+
|
4609
|
+
q.breakable("")
|
4610
|
+
q.text("]")
|
4624
4611
|
end
|
4625
4612
|
end
|
4626
4613
|
end
|
@@ -4823,6 +4810,7 @@ module SyntaxTree
|
|
4823
4810
|
q.indent do
|
4824
4811
|
q.breakable
|
4825
4812
|
q.seplist(assocs) { |assoc| q.format(assoc) }
|
4813
|
+
q.if_break { q.text(",") } if q.trailing_comma?
|
4826
4814
|
end
|
4827
4815
|
q.breakable
|
4828
4816
|
end
|
@@ -4891,17 +4879,9 @@ module SyntaxTree
|
|
4891
4879
|
end
|
4892
4880
|
|
4893
4881
|
def format(q)
|
4894
|
-
# This is a very specific behavior
|
4895
|
-
#
|
4896
|
-
|
4897
|
-
breakable = -> do
|
4898
|
-
q.target << PrettyPrint::Breakable.new(
|
4899
|
-
" ",
|
4900
|
-
1,
|
4901
|
-
indent: false,
|
4902
|
-
force: true
|
4903
|
-
)
|
4904
|
-
end
|
4882
|
+
# This is a very specific behavior where you want to force a newline, but
|
4883
|
+
# don't want to force the break parent.
|
4884
|
+
breakable = -> { q.breakable(indent: false, force: :skip_break_parent) }
|
4905
4885
|
|
4906
4886
|
q.group do
|
4907
4887
|
q.format(beginning)
|
@@ -5197,6 +5177,8 @@ module SyntaxTree
|
|
5197
5177
|
case node
|
5198
5178
|
in predicate: Assign | Command | CommandCall | MAssign | OpAssign
|
5199
5179
|
false
|
5180
|
+
in predicate: Not[parentheses: false]
|
5181
|
+
false
|
5200
5182
|
in {
|
5201
5183
|
statements: { body: [truthy] },
|
5202
5184
|
consequent: Else[statements: { body: [falsy] }]
|
@@ -5325,9 +5307,8 @@ module SyntaxTree
|
|
5325
5307
|
# force it into the output but we _don't_ want to explicitly
|
5326
5308
|
# break the parent. If a break-parent shows up in the tree, then
|
5327
5309
|
# it's going to force it all the way up to the tree, which is
|
5328
|
-
# going to negate the ternary.
|
5329
|
-
|
5330
|
-
q.target << PrettyPrint::Breakable.new(" ", 1, force: true)
|
5310
|
+
# going to negate the ternary.
|
5311
|
+
q.breakable(force: :skip_break_parent)
|
5331
5312
|
q.format(node.consequent.statements)
|
5332
5313
|
end
|
5333
5314
|
end
|
@@ -5956,7 +5937,7 @@ module SyntaxTree
|
|
5956
5937
|
# ->(value) { value * 2 }
|
5957
5938
|
#
|
5958
5939
|
class Lambda < Node
|
5959
|
-
# [
|
5940
|
+
# [LambdaVar | Paren] the parameter declaration for this lambda
|
5960
5941
|
attr_reader :params
|
5961
5942
|
|
5962
5943
|
# [BodyStmt | Statements] the expressions to be executed in this lambda
|
@@ -6011,24 +5992,100 @@ module SyntaxTree
|
|
6011
5992
|
node.is_a?(Command) || node.is_a?(CommandCall)
|
6012
5993
|
end
|
6013
5994
|
|
6014
|
-
|
6015
|
-
|
5995
|
+
if force_parens
|
5996
|
+
q.text("{")
|
5997
|
+
|
5998
|
+
unless statements.empty?
|
5999
|
+
q.indent do
|
6000
|
+
q.breakable
|
6001
|
+
q.format(statements)
|
6002
|
+
end
|
6003
|
+
q.breakable
|
6004
|
+
end
|
6005
|
+
|
6006
|
+
q.text("}")
|
6007
|
+
else
|
6008
|
+
q.text("do")
|
6009
|
+
|
6010
|
+
unless statements.empty?
|
6011
|
+
q.indent do
|
6012
|
+
q.breakable
|
6013
|
+
q.format(statements)
|
6014
|
+
end
|
6015
|
+
end
|
6016
|
+
|
6016
6017
|
q.breakable
|
6017
|
-
q.
|
6018
|
+
q.text("end")
|
6018
6019
|
end
|
6019
|
-
|
6020
|
-
q.breakable
|
6021
|
-
q.text(force_parens ? "}" : "end")
|
6022
6020
|
end
|
6023
6021
|
.if_flat do
|
6024
|
-
q.text("{
|
6025
|
-
|
6026
|
-
|
6022
|
+
q.text("{")
|
6023
|
+
|
6024
|
+
unless statements.empty?
|
6025
|
+
q.text(" ")
|
6026
|
+
q.format(statements)
|
6027
|
+
q.text(" ")
|
6028
|
+
end
|
6029
|
+
|
6030
|
+
q.text("}")
|
6027
6031
|
end
|
6028
6032
|
end
|
6029
6033
|
end
|
6030
6034
|
end
|
6031
6035
|
|
6036
|
+
# LambdaVar represents the parameters being declared for a lambda. Effectively
|
6037
|
+
# this node is everything contained within the parentheses. This includes all
|
6038
|
+
# of the various parameter types, as well as block-local variable
|
6039
|
+
# declarations.
|
6040
|
+
#
|
6041
|
+
# -> (positional, optional = value, keyword:, █ local) do
|
6042
|
+
# end
|
6043
|
+
#
|
6044
|
+
class LambdaVar < Node
|
6045
|
+
# [Params] the parameters being declared with the block
|
6046
|
+
attr_reader :params
|
6047
|
+
|
6048
|
+
# [Array[ Ident ]] the list of block-local variable declarations
|
6049
|
+
attr_reader :locals
|
6050
|
+
|
6051
|
+
# [Array[ Comment | EmbDoc ]] the comments attached to this node
|
6052
|
+
attr_reader :comments
|
6053
|
+
|
6054
|
+
def initialize(params:, locals:, location:, comments: [])
|
6055
|
+
@params = params
|
6056
|
+
@locals = locals
|
6057
|
+
@location = location
|
6058
|
+
@comments = comments
|
6059
|
+
end
|
6060
|
+
|
6061
|
+
def accept(visitor)
|
6062
|
+
visitor.visit_lambda_var(self)
|
6063
|
+
end
|
6064
|
+
|
6065
|
+
def child_nodes
|
6066
|
+
[params, *locals]
|
6067
|
+
end
|
6068
|
+
|
6069
|
+
alias deconstruct child_nodes
|
6070
|
+
|
6071
|
+
def deconstruct_keys(_keys)
|
6072
|
+
{ params: params, locals: locals, location: location, comments: comments }
|
6073
|
+
end
|
6074
|
+
|
6075
|
+
def empty?
|
6076
|
+
params.empty? && locals.empty?
|
6077
|
+
end
|
6078
|
+
|
6079
|
+
def format(q)
|
6080
|
+
q.format(params)
|
6081
|
+
|
6082
|
+
if locals.any?
|
6083
|
+
q.text("; ")
|
6084
|
+
q.seplist(locals, -> { q.text(", ") }) { |local| q.format(local) }
|
6085
|
+
end
|
6086
|
+
end
|
6087
|
+
end
|
6088
|
+
|
6032
6089
|
# LBrace represents the use of a left brace, i.e., {.
|
6033
6090
|
class LBrace < Node
|
6034
6091
|
# [String] the left brace
|
@@ -8314,8 +8371,7 @@ module SyntaxTree
|
|
8314
8371
|
# same line in the source, then we're going to leave them in place and
|
8315
8372
|
# assume that's the way the developer wanted this expression
|
8316
8373
|
# represented.
|
8317
|
-
|
8318
|
-
RemoveBreaks.call(doc)
|
8374
|
+
q.remove_breaks(q.group(0, '#{', "}") { q.format(statements) })
|
8319
8375
|
else
|
8320
8376
|
q.group do
|
8321
8377
|
q.text('#{')
|
data/lib/syntax_tree/parser.rb
CHANGED
@@ -1940,6 +1940,41 @@ module SyntaxTree
|
|
1940
1940
|
token.location.start_char > beginning.location.start_char
|
1941
1941
|
end
|
1942
1942
|
|
1943
|
+
# We need to do some special mapping here. Since ripper doesn't support
|
1944
|
+
# capturing lambda var until 3.2, we need to normalize all of that here.
|
1945
|
+
params =
|
1946
|
+
case params
|
1947
|
+
in Paren[contents: Params]
|
1948
|
+
# In this case we've gotten to the <3.2 parentheses wrapping a set of
|
1949
|
+
# parameters case. Here we need to manually scan for lambda locals.
|
1950
|
+
range = (params.location.start_char + 1)...params.location.end_char
|
1951
|
+
locals = lambda_locals(source[range])
|
1952
|
+
|
1953
|
+
location = params.contents.location
|
1954
|
+
location = location.to(locals.last.location) if locals.any?
|
1955
|
+
|
1956
|
+
Paren.new(
|
1957
|
+
lparen: params.lparen,
|
1958
|
+
contents:
|
1959
|
+
LambdaVar.new(
|
1960
|
+
params: params.contents,
|
1961
|
+
locals: locals,
|
1962
|
+
location: location
|
1963
|
+
),
|
1964
|
+
location: params.location,
|
1965
|
+
comments: params.comments
|
1966
|
+
)
|
1967
|
+
in Params
|
1968
|
+
# In this case we've gotten to the <3.2 plain set of parameters. In
|
1969
|
+
# this case there cannot be lambda locals, so we will wrap the
|
1970
|
+
# parameters into a lambda var that has no locals.
|
1971
|
+
LambdaVar.new(params: params, locals: [], location: params.location)
|
1972
|
+
in LambdaVar
|
1973
|
+
# In this case we've gotten to 3.2+ lambda var. In this case we don't
|
1974
|
+
# need to do anything and can just the value as given.
|
1975
|
+
params
|
1976
|
+
end
|
1977
|
+
|
1943
1978
|
if braces
|
1944
1979
|
opening = find_token(TLamBeg)
|
1945
1980
|
closing = find_token(RBrace)
|
@@ -1962,6 +1997,83 @@ module SyntaxTree
|
|
1962
1997
|
)
|
1963
1998
|
end
|
1964
1999
|
|
2000
|
+
# :call-seq:
|
2001
|
+
# on_lambda_var: (Params params, Array[ Ident ] locals) -> LambdaVar
|
2002
|
+
def on_lambda_var(params, locals)
|
2003
|
+
location = params.location
|
2004
|
+
location = location.to(locals.last.location) if locals.any?
|
2005
|
+
|
2006
|
+
LambdaVar.new(params: params, locals: locals || [], location: location)
|
2007
|
+
end
|
2008
|
+
|
2009
|
+
# Ripper doesn't support capturing lambda local variables until 3.2. To
|
2010
|
+
# mitigate this, we have to parse that code for ourselves. We use the range
|
2011
|
+
# from the parentheses to find where we _should_ be looking. Then we check
|
2012
|
+
# if the resulting tokens match a pattern that we determine means that the
|
2013
|
+
# declaration has block-local variables. Once it does, we parse those out
|
2014
|
+
# and convert them into Ident nodes.
|
2015
|
+
def lambda_locals(source)
|
2016
|
+
tokens = Ripper.lex(source)
|
2017
|
+
|
2018
|
+
# First, check that we have a semi-colon. If we do, then we can start to
|
2019
|
+
# parse the tokens _after_ the semicolon.
|
2020
|
+
index = tokens.rindex { |token| token[1] == :on_semicolon }
|
2021
|
+
return [] unless index
|
2022
|
+
|
2023
|
+
# Next, map over the tokens and convert them into Ident nodes. Bail out
|
2024
|
+
# midway through if we encounter a token we didn't expect. Basically we're
|
2025
|
+
# making our own mini-parser here. To do that we'll walk through a small
|
2026
|
+
# state machine:
|
2027
|
+
#
|
2028
|
+
# ┌────────┐ ┌────────┐ ┌─────────┐
|
2029
|
+
# │ │ │ │ │┌───────┐│
|
2030
|
+
# ──> │ item │ ─── ident ──> │ next │ ─── rparen ──> ││ final ││
|
2031
|
+
# │ │ <── comma ─── │ │ │└───────┘│
|
2032
|
+
# └────────┘ └────────┘ └─────────┘
|
2033
|
+
# │ ^ │ ^
|
2034
|
+
# └──┘ └──┘
|
2035
|
+
# ignored_nl, sp nl, sp
|
2036
|
+
#
|
2037
|
+
state = :item
|
2038
|
+
transitions = {
|
2039
|
+
item: {
|
2040
|
+
on_ignored_nl: :item,
|
2041
|
+
on_sp: :item,
|
2042
|
+
on_ident: :next
|
2043
|
+
},
|
2044
|
+
next: {
|
2045
|
+
on_nl: :next,
|
2046
|
+
on_sp: :next,
|
2047
|
+
on_comma: :item,
|
2048
|
+
on_rparen: :final
|
2049
|
+
},
|
2050
|
+
final: {
|
2051
|
+
}
|
2052
|
+
}
|
2053
|
+
|
2054
|
+
tokens[(index + 1)..].each_with_object([]) do |token, locals|
|
2055
|
+
(lineno, column), type, value, = token
|
2056
|
+
|
2057
|
+
# Make the state transition for the parser. If there isn't a transition
|
2058
|
+
# from the current state to a new state for this type, then we're in a
|
2059
|
+
# pattern that isn't actually locals. In that case we can return [].
|
2060
|
+
state = transitions[state].fetch(type) { return [] }
|
2061
|
+
|
2062
|
+
# If we hit an identifier, then add it to our list.
|
2063
|
+
next if type != :on_ident
|
2064
|
+
|
2065
|
+
location =
|
2066
|
+
Location.token(
|
2067
|
+
line: lineno,
|
2068
|
+
char: line_counts[lineno - 1][column],
|
2069
|
+
column: column,
|
2070
|
+
size: value.size
|
2071
|
+
)
|
2072
|
+
|
2073
|
+
locals << Ident.new(value: value, location: location)
|
2074
|
+
end
|
2075
|
+
end
|
2076
|
+
|
1965
2077
|
# :call-seq:
|
1966
2078
|
# on_lbrace: (String value) -> LBrace
|
1967
2079
|
def on_lbrace(value)
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rake"
|
4
|
+
require "rake/tasklib"
|
5
|
+
|
6
|
+
require "syntax_tree"
|
7
|
+
require "syntax_tree/cli"
|
8
|
+
|
9
|
+
module SyntaxTree
|
10
|
+
module Rake
|
11
|
+
# A Rake task that runs check on a set of source files.
|
12
|
+
#
|
13
|
+
# Example:
|
14
|
+
#
|
15
|
+
# require 'syntax_tree/rake/check_task'
|
16
|
+
#
|
17
|
+
# SyntaxTree::Rake::CheckTask.new do |t|
|
18
|
+
# t.source_files = '{app,config,lib}/**/*.rb'
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# This will create task that can be run with:
|
22
|
+
#
|
23
|
+
# rake stree_check
|
24
|
+
#
|
25
|
+
class CheckTask < ::Rake::TaskLib
|
26
|
+
# Name of the task.
|
27
|
+
# Defaults to :"stree:check".
|
28
|
+
attr_accessor :name
|
29
|
+
|
30
|
+
# Glob pattern to match source files.
|
31
|
+
# Defaults to 'lib/**/*.rb'.
|
32
|
+
attr_accessor :source_files
|
33
|
+
|
34
|
+
# The set of plugins to require.
|
35
|
+
# Defaults to [].
|
36
|
+
attr_accessor :plugins
|
37
|
+
|
38
|
+
def initialize(
|
39
|
+
name = :"stree:check",
|
40
|
+
source_files = ::Rake::FileList["lib/**/*.rb"],
|
41
|
+
plugins = []
|
42
|
+
)
|
43
|
+
@name = name
|
44
|
+
@source_files = source_files
|
45
|
+
@plugins = plugins
|
46
|
+
|
47
|
+
yield self if block_given?
|
48
|
+
define_task
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def define_task
|
54
|
+
desc "Runs `stree check` over source files"
|
55
|
+
task(name) { run_task }
|
56
|
+
end
|
57
|
+
|
58
|
+
def run_task
|
59
|
+
arguments = ["check"]
|
60
|
+
arguments << "--plugins=#{plugins.join(",")}" if plugins.any?
|
61
|
+
|
62
|
+
SyntaxTree::CLI.run(arguments + Array(source_files))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|