collie 0.1.0 → 1.0.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/CHANGELOG.md +28 -1
- data/README.md +55 -258
- data/lib/collie/analyzer/reachability.rb +17 -20
- data/lib/collie/analyzer/recursion.rb +28 -9
- data/lib/collie/analyzer/symbol_resolver.rb +51 -0
- data/lib/collie/ast.rb +18 -4
- data/lib/collie/cli.rb +388 -50
- data/lib/collie/config/schema.rb +117 -0
- data/lib/collie/config.rb +106 -22
- data/lib/collie/formatter/formatter.rb +95 -50
- data/lib/collie/formatter/options.rb +17 -5
- data/lib/collie/formatter/signature.rb +72 -0
- data/lib/collie/linter/base.rb +49 -0
- data/lib/collie/linter/rules/ambiguous_precedence.rb +5 -2
- data/lib/collie/linter/rules/circular_reference.rb +96 -38
- data/lib/collie/linter/rules/consistent_tag_naming.rb +13 -13
- data/lib/collie/linter/rules/empty_action.rb +42 -11
- data/lib/collie/linter/rules/factorizable_rules.rb +2 -2
- data/lib/collie/linter/rules/left_recursion.rb +5 -4
- data/lib/collie/linter/rules/long_rule.rb +3 -3
- data/lib/collie/linter/rules/nonterminal_naming.rb +6 -4
- data/lib/collie/linter/rules/prec_improvement.rb +1 -1
- data/lib/collie/linter/rules/redundant_epsilon.rb +11 -11
- data/lib/collie/linter/rules/right_recursion.rb +4 -1
- data/lib/collie/linter/rules/symbol_conflict.rb +130 -0
- data/lib/collie/linter/rules/token_naming.rb +2 -1
- data/lib/collie/linter/rules/trailing_whitespace.rb +7 -1
- data/lib/collie/linter/rules/undefined_symbol.rb +50 -8
- data/lib/collie/linter/rules/unused_nonterminal.rb +36 -1
- data/lib/collie/linter/rules/unused_token.rb +34 -9
- data/lib/collie/parser/debug_serializer.rb +205 -0
- data/lib/collie/parser/lexer.rb +182 -11
- data/lib/collie/parser/parser.rb +73 -13
- data/lib/collie/reporter/github.rb +15 -2
- data/lib/collie/reporter/json.rb +4 -1
- data/lib/collie/reporter/sarif.rb +81 -0
- data/lib/collie/version.rb +1 -1
- data/lib/collie.rb +6 -1
- metadata +8 -2
|
@@ -53,8 +53,10 @@ module Collie
|
|
|
53
53
|
def collect_operators(ast)
|
|
54
54
|
operators = Hash.new { |h, k| h[k] = [] }
|
|
55
55
|
|
|
56
|
-
ast
|
|
56
|
+
each_rule_like(ast) do |rule|
|
|
57
57
|
rule.alternatives.each do |alt|
|
|
58
|
+
next if alt.prec
|
|
59
|
+
|
|
58
60
|
alt.symbols.each do |symbol|
|
|
59
61
|
next unless symbol.terminal?
|
|
60
62
|
next unless looks_like_operator?(symbol.name)
|
|
@@ -75,7 +77,8 @@ module Collie
|
|
|
75
77
|
offense = Offense.new(
|
|
76
78
|
rule: self.class,
|
|
77
79
|
location: location,
|
|
78
|
-
message: message
|
|
80
|
+
message: message,
|
|
81
|
+
severity: configured_severity
|
|
79
82
|
)
|
|
80
83
|
@offenses << offense
|
|
81
84
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
3
5
|
module Collie
|
|
4
6
|
module Linter
|
|
5
7
|
module Rules
|
|
@@ -12,14 +14,16 @@ module Collie
|
|
|
12
14
|
|
|
13
15
|
def check(ast, _context = {})
|
|
14
16
|
@rules_map = build_rules_map(ast)
|
|
15
|
-
@
|
|
16
|
-
|
|
17
|
+
@dependencies = build_dependency_graph
|
|
18
|
+
productive_rules = compute_productive_rules
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
next
|
|
20
|
+
strongly_connected_components.each do |component|
|
|
21
|
+
next unless cyclic_component?(component)
|
|
22
|
+
next if component.any? { |rule_name| productive_rules.include?(rule_name) }
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
component.each do |rule_name|
|
|
25
|
+
rule = @rules_map[rule_name]
|
|
26
|
+
add_offense(rule, message: "Rule '#{rule_name}' is part of a non-productive circular reference") if rule
|
|
23
27
|
end
|
|
24
28
|
end
|
|
25
29
|
|
|
@@ -29,57 +33,111 @@ module Collie
|
|
|
29
33
|
private
|
|
30
34
|
|
|
31
35
|
def build_rules_map(ast)
|
|
32
|
-
ast.rules.
|
|
33
|
-
|
|
36
|
+
rules = ast.rules + ast.declarations.select do |decl|
|
|
37
|
+
decl.is_a?(AST::ParameterizedRule) || decl.is_a?(AST::InlineRule)
|
|
34
38
|
end
|
|
35
|
-
end
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
rules.each_with_object({}) do |rule, map|
|
|
41
|
+
map[rule_name(rule)] = rule
|
|
42
|
+
end
|
|
43
|
+
end
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
def rule_name(rule)
|
|
46
|
+
rule.is_a?(AST::InlineRule) ? rule.rule : rule.name
|
|
47
|
+
end
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
def build_dependency_graph
|
|
50
|
+
@rules_map.transform_values do |rule|
|
|
51
|
+
rule.alternatives.each_with_object(Set.new) do |alternative, dependencies|
|
|
52
|
+
alternative.symbols.each { |symbol| collect_dependencies(symbol, dependencies) }
|
|
53
|
+
end
|
|
45
54
|
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def collect_dependencies(symbol, dependencies)
|
|
58
|
+
dependencies << symbol.name if symbol.nonterminal? && @rules_map.key?(symbol.name)
|
|
59
|
+
symbol.arguments&.each { |argument| collect_dependencies(argument, dependencies) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def compute_productive_rules
|
|
63
|
+
productive = Set.new
|
|
64
|
+
|
|
65
|
+
loop do
|
|
66
|
+
changed = false
|
|
67
|
+
|
|
68
|
+
@rules_map.each do |name, rule|
|
|
69
|
+
next if productive.include?(name)
|
|
70
|
+
next unless rule.alternatives.any? { |alternative| productive_alternative?(alternative, productive) }
|
|
46
71
|
|
|
47
|
-
|
|
48
|
-
|
|
72
|
+
productive << name
|
|
73
|
+
changed = true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
break unless changed
|
|
77
|
+
end
|
|
49
78
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
rule&.alternatives&.each do |alt|
|
|
53
|
-
# Skip alternatives with terminals or empty alternatives
|
|
54
|
-
next if has_terminal_or_empty?(alt)
|
|
79
|
+
productive
|
|
80
|
+
end
|
|
55
81
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
next unless first_symbol&.nonterminal?
|
|
82
|
+
def productive_alternative?(alternative, productive_rules)
|
|
83
|
+
return true if alternative.explicit_empty || alternative.symbols.empty?
|
|
59
84
|
|
|
60
|
-
|
|
85
|
+
alternative.symbols.all? do |symbol|
|
|
86
|
+
productive_symbol?(symbol, productive_rules)
|
|
61
87
|
end
|
|
88
|
+
end
|
|
62
89
|
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
def productive_symbol?(symbol, productive_rules)
|
|
91
|
+
return true if symbol.terminal?
|
|
65
92
|
|
|
66
|
-
|
|
93
|
+
symbol.nonterminal? && productive_rules.include?(symbol.name)
|
|
67
94
|
end
|
|
68
95
|
|
|
69
|
-
def
|
|
70
|
-
|
|
96
|
+
def strongly_connected_components
|
|
97
|
+
@index = 0
|
|
98
|
+
@indices = {}
|
|
99
|
+
@lowlinks = {}
|
|
100
|
+
@stack = []
|
|
101
|
+
@on_stack = Set.new
|
|
102
|
+
components = []
|
|
103
|
+
|
|
104
|
+
@rules_map.each_key do |rule_name|
|
|
105
|
+
strong_connect(rule_name, components) unless @indices.key?(rule_name)
|
|
106
|
+
end
|
|
71
107
|
|
|
72
|
-
|
|
108
|
+
components
|
|
73
109
|
end
|
|
74
110
|
|
|
75
|
-
def
|
|
76
|
-
|
|
77
|
-
|
|
111
|
+
def strong_connect(rule_name, components)
|
|
112
|
+
@indices[rule_name] = @index
|
|
113
|
+
@lowlinks[rule_name] = @index
|
|
114
|
+
@index += 1
|
|
115
|
+
@stack << rule_name
|
|
116
|
+
@on_stack << rule_name
|
|
117
|
+
|
|
118
|
+
@dependencies[rule_name].each do |dependency|
|
|
119
|
+
if !@indices.key?(dependency)
|
|
120
|
+
strong_connect(dependency, components)
|
|
121
|
+
@lowlinks[rule_name] = [@lowlinks[rule_name], @lowlinks[dependency]].min
|
|
122
|
+
elsif @on_stack.include?(dependency)
|
|
123
|
+
@lowlinks[rule_name] = [@lowlinks[rule_name], @indices[dependency]].min
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
return unless @lowlinks[rule_name] == @indices[rule_name]
|
|
78
128
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
129
|
+
component = []
|
|
130
|
+
loop do
|
|
131
|
+
dependency = @stack.pop
|
|
132
|
+
@on_stack.delete(dependency)
|
|
133
|
+
component << dependency
|
|
134
|
+
break if dependency == rule_name
|
|
82
135
|
end
|
|
136
|
+
components << component
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def cyclic_component?(component)
|
|
140
|
+
component.size > 1 || @dependencies[component.first].include?(component.first)
|
|
83
141
|
end
|
|
84
142
|
end
|
|
85
143
|
end
|
|
@@ -17,7 +17,7 @@ module Collie
|
|
|
17
17
|
styles = tags.group_by { |tag, _| detect_style(tag) }
|
|
18
18
|
|
|
19
19
|
# If we have multiple styles, report inconsistency
|
|
20
|
-
|
|
20
|
+
add_inconsistency_offenses(styles) if styles.size > 1
|
|
21
21
|
|
|
22
22
|
@offenses
|
|
23
23
|
end
|
|
@@ -45,21 +45,21 @@ module Collie
|
|
|
45
45
|
:other
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
Node = Struct.new(:location)
|
|
49
|
+
|
|
50
|
+
def add_inconsistency_offenses(styles)
|
|
49
51
|
style_names = styles.keys.map(&:to_s).join(", ")
|
|
50
52
|
most_common_style = styles.max_by { |_, tags| tags.size }[0]
|
|
53
|
+
expected_tags = styles.fetch(most_common_style)
|
|
54
|
+
outliers = styles.reject { |style, _| style == most_common_style }.values.flatten(1)
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
message: "Inconsistent type tag naming styles detected (#{style_names}). " \
|
|
60
|
-
"Consider using #{most_common_style} throughout."
|
|
61
|
-
)
|
|
62
|
-
@offenses << offense
|
|
56
|
+
outliers.each do |tag, location|
|
|
57
|
+
add_offense(
|
|
58
|
+
Node.new(location || expected_tags.first[1] || AST::Location.new(file: "grammar", line: 1, column: 1)),
|
|
59
|
+
message: "Type tag '#{tag}' uses a different naming style (#{style_names}). " \
|
|
60
|
+
"Consider using #{most_common_style} throughout."
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
end
|
|
@@ -10,9 +10,9 @@ module Collie
|
|
|
10
10
|
self.severity = :convention
|
|
11
11
|
self.autocorrectable = true
|
|
12
12
|
|
|
13
|
-
def check(ast,
|
|
14
|
-
ast
|
|
15
|
-
check_rule(rule)
|
|
13
|
+
def check(ast, context = {})
|
|
14
|
+
each_rule_like(ast) do |rule|
|
|
15
|
+
check_rule(rule, context)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
@offenses
|
|
@@ -20,30 +20,61 @@ module Collie
|
|
|
20
20
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
|
-
def check_rule(rule)
|
|
23
|
+
def check_rule(rule, context)
|
|
24
24
|
rule.alternatives.each do |alt|
|
|
25
25
|
next unless alt.action
|
|
26
26
|
next unless empty_action?(alt.action)
|
|
27
27
|
|
|
28
28
|
add_offense(
|
|
29
|
-
alt,
|
|
29
|
+
alt.action,
|
|
30
30
|
message: "Empty action block can be removed",
|
|
31
|
-
autocorrect: -> { remove_action(alt) }
|
|
31
|
+
autocorrect: -> { remove_action(alt, context) }
|
|
32
32
|
)
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def empty_action?(action)
|
|
37
|
-
|
|
38
|
-
return true
|
|
39
|
-
return true if action.code.strip.empty?
|
|
37
|
+
code = action.code.to_s.strip
|
|
38
|
+
return true if code.empty?
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
action_body = if code.start_with?("{") && code.end_with?("}")
|
|
41
|
+
code[1...-1].strip
|
|
42
|
+
else
|
|
43
|
+
code
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
action_body.empty?
|
|
42
47
|
end
|
|
43
48
|
|
|
44
|
-
def remove_action(alternative)
|
|
49
|
+
def remove_action(alternative, context)
|
|
50
|
+
action = alternative.action
|
|
51
|
+
if context[:source] && action&.location
|
|
52
|
+
context[:source] = remove_action_from_source(context[:source], action.location)
|
|
53
|
+
end
|
|
54
|
+
|
|
45
55
|
alternative.action = nil
|
|
46
56
|
end
|
|
57
|
+
|
|
58
|
+
def remove_action_from_source(source, location)
|
|
59
|
+
index = source_index(source, location)
|
|
60
|
+
return source unless index
|
|
61
|
+
|
|
62
|
+
prefix = source[0...index].sub(/[ \t]*\z/, "")
|
|
63
|
+
suffix = source[(index + location.length)..] || ""
|
|
64
|
+
"#{prefix}#{suffix}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def source_index(source, location)
|
|
68
|
+
offset = 0
|
|
69
|
+
|
|
70
|
+
source.each_line.with_index(1) do |line, line_number|
|
|
71
|
+
return offset + location.column - 1 if line_number == location.line
|
|
72
|
+
|
|
73
|
+
offset += line.length
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
47
78
|
end
|
|
48
79
|
end
|
|
49
80
|
end
|
|
@@ -13,7 +13,7 @@ module Collie
|
|
|
13
13
|
MIN_PREFIX_LENGTH = 2
|
|
14
14
|
|
|
15
15
|
def check(ast, _context = {})
|
|
16
|
-
ast
|
|
16
|
+
each_rule_like(ast) do |rule|
|
|
17
17
|
check_rule(rule)
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -37,7 +37,7 @@ module Collie
|
|
|
37
37
|
|
|
38
38
|
add_offense(
|
|
39
39
|
rule,
|
|
40
|
-
message: "Rule '#{rule
|
|
40
|
+
message: "Rule '#{rule_like_name(rule)}' has #{alternatives.size} alternatives with common prefix " \
|
|
41
41
|
"(#{prefix_length} symbols). Consider factoring."
|
|
42
42
|
)
|
|
43
43
|
break # Only report once per rule
|
|
@@ -6,8 +6,8 @@ module Collie
|
|
|
6
6
|
# Detects left recursion in grammar rules
|
|
7
7
|
class LeftRecursion < Base
|
|
8
8
|
self.rule_name = "LeftRecursion"
|
|
9
|
-
self.description = "
|
|
10
|
-
self.severity = :
|
|
9
|
+
self.description = "Notes left recursion for LL parser portability"
|
|
10
|
+
self.severity = :info
|
|
11
11
|
self.autocorrectable = false
|
|
12
12
|
|
|
13
13
|
def check(ast, _context = {})
|
|
@@ -15,12 +15,13 @@ module Collie
|
|
|
15
15
|
result = analyzer.analyze
|
|
16
16
|
|
|
17
17
|
result[:left_recursive].each do |rule_name|
|
|
18
|
-
rule = ast
|
|
18
|
+
rule = find_rule_like(ast, rule_name)
|
|
19
19
|
next unless rule
|
|
20
20
|
|
|
21
21
|
add_offense(
|
|
22
22
|
rule,
|
|
23
|
-
message: "Rule '#{rule_name}' uses left recursion
|
|
23
|
+
message: "Rule '#{rule_name}' uses left recursion. This is normal for LR parsers; " \
|
|
24
|
+
"review only if targeting LL parser portability."
|
|
24
25
|
)
|
|
25
26
|
end
|
|
26
27
|
|
|
@@ -13,16 +13,16 @@ module Collie
|
|
|
13
13
|
DEFAULT_MAX_ALTERNATIVES = 10
|
|
14
14
|
|
|
15
15
|
def check(ast, _context = {})
|
|
16
|
-
max_alternatives =
|
|
16
|
+
max_alternatives = config_value(:max_alternatives, DEFAULT_MAX_ALTERNATIVES)
|
|
17
17
|
|
|
18
|
-
ast
|
|
18
|
+
each_rule_like(ast) do |rule|
|
|
19
19
|
alternatives_count = rule.alternatives.size
|
|
20
20
|
|
|
21
21
|
next unless alternatives_count > max_alternatives
|
|
22
22
|
|
|
23
23
|
add_offense(
|
|
24
24
|
rule,
|
|
25
|
-
message: "Rule '#{rule
|
|
25
|
+
message: "Rule '#{rule_like_name(rule)}' has #{alternatives_count} alternatives " \
|
|
26
26
|
"(max: #{max_alternatives}). Consider refactoring."
|
|
27
27
|
)
|
|
28
28
|
end
|
|
@@ -15,13 +15,15 @@ module Collie
|
|
|
15
15
|
DEFAULT_PATTERN = /^[a-z][a-z0-9_]*$/
|
|
16
16
|
|
|
17
17
|
def check(ast, _context = {})
|
|
18
|
-
|
|
18
|
+
pattern_config = config_value(:pattern)
|
|
19
|
+
pattern = pattern_config ? Regexp.new(pattern_config) : DEFAULT_PATTERN
|
|
19
20
|
|
|
20
|
-
ast
|
|
21
|
-
|
|
21
|
+
each_rule_like(ast) do |rule|
|
|
22
|
+
name = rule_like_name(rule)
|
|
23
|
+
next if name.match?(pattern)
|
|
22
24
|
|
|
23
25
|
add_offense(rule,
|
|
24
|
-
message: "Nonterminal '#{
|
|
26
|
+
message: "Nonterminal '#{name}' should match pattern #{pattern.inspect}")
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
@offenses
|
|
@@ -6,12 +6,12 @@ module Collie
|
|
|
6
6
|
# Detects potentially redundant epsilon productions
|
|
7
7
|
class RedundantEpsilon < Base
|
|
8
8
|
self.rule_name = "RedundantEpsilon"
|
|
9
|
-
self.description = "Detects
|
|
9
|
+
self.description = "Detects duplicate epsilon (empty) productions"
|
|
10
10
|
self.severity = :info
|
|
11
11
|
self.autocorrectable = false
|
|
12
12
|
|
|
13
13
|
def check(ast, _context = {})
|
|
14
|
-
ast
|
|
14
|
+
each_rule_like(ast) do |rule|
|
|
15
15
|
check_rule(rule)
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -21,21 +21,21 @@ module Collie
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
23
|
def check_rule(rule)
|
|
24
|
-
epsilon_alternatives = rule.alternatives.select { |alt| alt
|
|
25
|
-
return if epsilon_alternatives.
|
|
24
|
+
epsilon_alternatives = rule.alternatives.select { |alt| epsilon?(alt) }
|
|
25
|
+
return if epsilon_alternatives.size < 2
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
non_epsilon_alternatives = rule.alternatives.reject { |alt| alt.symbols.empty? }
|
|
29
|
-
return if non_epsilon_alternatives.empty?
|
|
30
|
-
|
|
31
|
-
epsilon_alternatives.each do |alt|
|
|
27
|
+
epsilon_alternatives.drop(1).each do |alt|
|
|
32
28
|
add_offense(
|
|
33
29
|
alt,
|
|
34
|
-
message: "Rule '#{rule
|
|
35
|
-
"
|
|
30
|
+
message: "Rule '#{rule_like_name(rule)}' has multiple epsilon productions. " \
|
|
31
|
+
"Keep one empty alternative and remove duplicates."
|
|
36
32
|
)
|
|
37
33
|
end
|
|
38
34
|
end
|
|
35
|
+
|
|
36
|
+
def epsilon?(alternative)
|
|
37
|
+
alternative.symbols.empty? || alternative.explicit_empty
|
|
38
|
+
end
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
end
|
|
@@ -13,9 +13,12 @@ module Collie
|
|
|
13
13
|
def check(ast, _context = {})
|
|
14
14
|
analyzer = Analyzer::Recursion.new(ast)
|
|
15
15
|
result = analyzer.analyze
|
|
16
|
+
left_recursive = result[:left_recursive]
|
|
16
17
|
|
|
17
18
|
result[:right_recursive].each do |rule_name|
|
|
18
|
-
|
|
19
|
+
next if left_recursive.include?(rule_name)
|
|
20
|
+
|
|
21
|
+
rule = find_rule_like(ast, rule_name)
|
|
19
22
|
next unless rule
|
|
20
23
|
|
|
21
24
|
add_offense(
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base"
|
|
4
|
+
|
|
5
|
+
module Collie
|
|
6
|
+
module Linter
|
|
7
|
+
module Rules
|
|
8
|
+
# Detects conflicting grammar symbol declarations.
|
|
9
|
+
class SymbolConflict < Base
|
|
10
|
+
self.rule_name = "SymbolConflict"
|
|
11
|
+
self.description = "Detects conflicting token and nonterminal declarations"
|
|
12
|
+
self.severity = :error
|
|
13
|
+
self.autocorrectable = false
|
|
14
|
+
|
|
15
|
+
def check(ast, _context = {})
|
|
16
|
+
tokens = collect_tokens(ast)
|
|
17
|
+
nonterminals = collect_nonterminals(ast)
|
|
18
|
+
precedence_tokens = collect_precedence_tokens(ast)
|
|
19
|
+
|
|
20
|
+
report_token_nonterminal_conflicts(tokens, nonterminals)
|
|
21
|
+
report_duplicate_nonterminals(nonterminals)
|
|
22
|
+
report_duplicate_precedence_tokens(precedence_tokens)
|
|
23
|
+
|
|
24
|
+
@offenses
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
Entry = Struct.new(:name, :location)
|
|
30
|
+
|
|
31
|
+
def collect_tokens(ast)
|
|
32
|
+
ast.declarations.each_with_object({}) do |decl, tokens|
|
|
33
|
+
token_names(decl).each do |name|
|
|
34
|
+
tokens[name] ||= Entry.new(name, decl.location)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def token_names(declaration)
|
|
40
|
+
case declaration
|
|
41
|
+
when AST::TokenDeclaration
|
|
42
|
+
declaration.names
|
|
43
|
+
when AST::PrecedenceDeclaration
|
|
44
|
+
declaration.tokens
|
|
45
|
+
else
|
|
46
|
+
[]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def collect_nonterminals(ast)
|
|
51
|
+
entries = []
|
|
52
|
+
|
|
53
|
+
ast.declarations.each do |decl|
|
|
54
|
+
case decl
|
|
55
|
+
when AST::ParameterizedRule
|
|
56
|
+
entries << Entry.new(decl.name, decl.location)
|
|
57
|
+
when AST::InlineRule
|
|
58
|
+
entries << Entry.new(decl.rule, decl.location)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
ast.rules.each do |rule|
|
|
63
|
+
entries << Entry.new(rule.name, rule.location)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
entries
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def collect_precedence_tokens(ast)
|
|
70
|
+
entries = []
|
|
71
|
+
|
|
72
|
+
ast.declarations.each do |decl|
|
|
73
|
+
next unless decl.is_a?(AST::PrecedenceDeclaration)
|
|
74
|
+
|
|
75
|
+
decl.tokens.each do |name|
|
|
76
|
+
entries << Entry.new(name, decl.location)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
entries
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def report_token_nonterminal_conflicts(tokens, nonterminals)
|
|
84
|
+
nonterminals.each do |entry|
|
|
85
|
+
next unless tokens.key?(entry.name)
|
|
86
|
+
|
|
87
|
+
add_offense(
|
|
88
|
+
Node.new(entry.location),
|
|
89
|
+
message: "Symbol '#{entry.name}' is declared as both token and nonterminal"
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def report_duplicate_nonterminals(nonterminals)
|
|
95
|
+
seen = {}
|
|
96
|
+
|
|
97
|
+
nonterminals.each do |entry|
|
|
98
|
+
if seen.key?(entry.name)
|
|
99
|
+
add_offense(
|
|
100
|
+
Node.new(entry.location),
|
|
101
|
+
message: "Nonterminal '#{entry.name}' already defined at #{seen[entry.name]}"
|
|
102
|
+
)
|
|
103
|
+
else
|
|
104
|
+
seen[entry.name] = entry.location
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def report_duplicate_precedence_tokens(precedence_tokens)
|
|
110
|
+
seen = {}
|
|
111
|
+
|
|
112
|
+
precedence_tokens.each do |entry|
|
|
113
|
+
if seen.key?(entry.name)
|
|
114
|
+
add_offense(
|
|
115
|
+
Node.new(entry.location),
|
|
116
|
+
message: "Precedence token '#{entry.name}' already declared at #{seen[entry.name]}"
|
|
117
|
+
)
|
|
118
|
+
else
|
|
119
|
+
seen[entry.name] = entry.location
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
Node = Struct.new(:location)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
Collie::Linter::Registry.register(Collie::Linter::Rules::SymbolConflict)
|
|
@@ -15,7 +15,8 @@ module Collie
|
|
|
15
15
|
DEFAULT_PATTERN = /^[A-Z][A-Z0-9_]*$/
|
|
16
16
|
|
|
17
17
|
def check(ast, _context = {})
|
|
18
|
-
|
|
18
|
+
pattern_config = config_value(:pattern)
|
|
19
|
+
pattern = pattern_config ? Regexp.new(pattern_config) : DEFAULT_PATTERN
|
|
19
20
|
|
|
20
21
|
ast.declarations.each do |decl|
|
|
21
22
|
next unless decl.is_a?(AST::TokenDeclaration)
|
|
@@ -31,13 +31,19 @@ module Collie
|
|
|
31
31
|
Node.new(location),
|
|
32
32
|
message: "Trailing whitespace detected",
|
|
33
33
|
autocorrect: lambda {
|
|
34
|
-
context[:source] =
|
|
34
|
+
context[:source] = remove_trailing_whitespace(context[:source] || source)
|
|
35
35
|
}
|
|
36
36
|
)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
@offenses
|
|
40
40
|
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def remove_trailing_whitespace(source)
|
|
45
|
+
source.gsub(/[ \t]+\n/, "\n").gsub(/[ \t]+$/, "")
|
|
46
|
+
end
|
|
41
47
|
end
|
|
42
48
|
end
|
|
43
49
|
end
|