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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collie
|
|
4
|
+
class Config
|
|
5
|
+
# JSON Schema for .collie.yml files.
|
|
6
|
+
module Schema
|
|
7
|
+
SEVERITIES = %w[error warning convention info].freeze
|
|
8
|
+
|
|
9
|
+
SEVERITY_SCHEMA = {
|
|
10
|
+
"type" => "string",
|
|
11
|
+
"enum" => SEVERITIES
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
FORMATTER_PROPERTIES = {
|
|
15
|
+
"indent_size" => {
|
|
16
|
+
"type" => "integer",
|
|
17
|
+
"minimum" => 1
|
|
18
|
+
},
|
|
19
|
+
"align_tokens" => {
|
|
20
|
+
"type" => "boolean"
|
|
21
|
+
},
|
|
22
|
+
"align_alternatives" => {
|
|
23
|
+
"type" => "boolean"
|
|
24
|
+
},
|
|
25
|
+
"blank_lines_around_sections" => {
|
|
26
|
+
"type" => "integer",
|
|
27
|
+
"minimum" => 0
|
|
28
|
+
},
|
|
29
|
+
"max_line_length" => {
|
|
30
|
+
"type" => "integer",
|
|
31
|
+
"minimum" => 1
|
|
32
|
+
}
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
def to_h
|
|
37
|
+
{
|
|
38
|
+
"$schema" => "https://json-schema.org/draft/2020-12/schema",
|
|
39
|
+
"title" => "Collie configuration",
|
|
40
|
+
"type" => "object",
|
|
41
|
+
"additionalProperties" => false,
|
|
42
|
+
"properties" => properties,
|
|
43
|
+
"$defs" => definitions
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def properties
|
|
50
|
+
{
|
|
51
|
+
"inherit_from" => {
|
|
52
|
+
"type" => "string"
|
|
53
|
+
},
|
|
54
|
+
"include" => string_array_schema,
|
|
55
|
+
"exclude" => string_array_schema,
|
|
56
|
+
"formatter" => formatter_schema,
|
|
57
|
+
"rules" => rules_schema
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def formatter_schema
|
|
62
|
+
{
|
|
63
|
+
"type" => "object",
|
|
64
|
+
"additionalProperties" => false,
|
|
65
|
+
"properties" => FORMATTER_PROPERTIES
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def rules_schema
|
|
70
|
+
{
|
|
71
|
+
"type" => "object",
|
|
72
|
+
"additionalProperties" => { "$ref" => "#/$defs/ruleConfig" },
|
|
73
|
+
"propertyNames" => {
|
|
74
|
+
"pattern" => "^[A-Za-z][A-Za-z0-9_]*$"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def definitions
|
|
80
|
+
{
|
|
81
|
+
"severity" => SEVERITY_SCHEMA,
|
|
82
|
+
"ruleConfig" => {
|
|
83
|
+
"oneOf" => [
|
|
84
|
+
{ "type" => "boolean" },
|
|
85
|
+
rule_object_schema
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def rule_object_schema
|
|
92
|
+
{
|
|
93
|
+
"type" => "object",
|
|
94
|
+
"additionalProperties" => true,
|
|
95
|
+
"properties" => {
|
|
96
|
+
"enabled" => { "type" => "boolean" },
|
|
97
|
+
"severity" => { "$ref" => "#/$defs/severity" },
|
|
98
|
+
"pattern" => { "type" => "string" },
|
|
99
|
+
"max_alternatives" => {
|
|
100
|
+
"type" => "integer",
|
|
101
|
+
"minimum" => 1
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def string_array_schema
|
|
108
|
+
{
|
|
109
|
+
"type" => "array",
|
|
110
|
+
"items" => { "type" => "string" },
|
|
111
|
+
"uniqueItems" => true
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
data/lib/collie/config.rb
CHANGED
|
@@ -18,6 +18,62 @@ module Collie
|
|
|
18
18
|
"exclude" => ["vendor/**/*", "tmp/**/*"]
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
21
|
+
PROFILE_OVERRIDES = {
|
|
22
|
+
"default" => {},
|
|
23
|
+
"lrama" => {
|
|
24
|
+
"rules" => {
|
|
25
|
+
"LeftRecursion" => { "enabled" => false },
|
|
26
|
+
"FactorizableRules" => { "enabled" => false },
|
|
27
|
+
"RightRecursion" => { "severity" => "warning" }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"bison" => {
|
|
31
|
+
"rules" => {
|
|
32
|
+
"LeftRecursion" => { "enabled" => false },
|
|
33
|
+
"FactorizableRules" => { "enabled" => false },
|
|
34
|
+
"RightRecursion" => { "severity" => "warning" }
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"strict" => {
|
|
38
|
+
"formatter" => {
|
|
39
|
+
"max_line_length" => 100
|
|
40
|
+
},
|
|
41
|
+
"rules" => {
|
|
42
|
+
"AmbiguousPrecedence" => { "severity" => "warning" },
|
|
43
|
+
"ConsistentTagNaming" => { "severity" => "warning" },
|
|
44
|
+
"EmptyAction" => { "severity" => "warning" },
|
|
45
|
+
"FactorizableRules" => { "severity" => "warning" },
|
|
46
|
+
"LongRule" => { "severity" => "warning" },
|
|
47
|
+
"NonterminalNaming" => { "severity" => "warning" },
|
|
48
|
+
"PrecImprovement" => { "severity" => "warning" },
|
|
49
|
+
"RedundantEpsilon" => { "severity" => "warning" },
|
|
50
|
+
"TokenNaming" => { "severity" => "warning" },
|
|
51
|
+
"TrailingWhitespace" => { "severity" => "warning" }
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"minimal" => {
|
|
55
|
+
"rules" => {
|
|
56
|
+
"AmbiguousPrecedence" => { "enabled" => false },
|
|
57
|
+
"ConsistentTagNaming" => { "enabled" => false },
|
|
58
|
+
"EmptyAction" => { "enabled" => false },
|
|
59
|
+
"FactorizableRules" => { "enabled" => false },
|
|
60
|
+
"LeftRecursion" => { "enabled" => false },
|
|
61
|
+
"LongRule" => { "enabled" => false },
|
|
62
|
+
"NonterminalNaming" => { "enabled" => false },
|
|
63
|
+
"PrecImprovement" => { "enabled" => false },
|
|
64
|
+
"RedundantEpsilon" => { "enabled" => false },
|
|
65
|
+
"RightRecursion" => { "enabled" => false },
|
|
66
|
+
"TokenNaming" => { "enabled" => false },
|
|
67
|
+
"TrailingWhitespace" => { "enabled" => false },
|
|
68
|
+
"UnreachableRule" => { "enabled" => false },
|
|
69
|
+
"UnusedNonterminal" => { "enabled" => false },
|
|
70
|
+
"UnusedToken" => { "enabled" => false }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}.freeze
|
|
74
|
+
|
|
75
|
+
PROFILE_NAMES = PROFILE_OVERRIDES.keys.freeze
|
|
76
|
+
|
|
21
77
|
attr_reader :config
|
|
22
78
|
|
|
23
79
|
def initialize(config_path = nil)
|
|
@@ -51,34 +107,22 @@ module Collie
|
|
|
51
107
|
new
|
|
52
108
|
end
|
|
53
109
|
|
|
54
|
-
def self.generate_default(path = ".collie.yml")
|
|
55
|
-
File.write(path,
|
|
110
|
+
def self.generate_default(path = ".collie.yml", profile: "default")
|
|
111
|
+
File.write(path, profile_config(profile).to_yaml)
|
|
56
112
|
end
|
|
57
113
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
config = DEFAULT_CONFIG.dup
|
|
62
|
-
|
|
63
|
-
if config_path && File.exist?(config_path)
|
|
64
|
-
user_config = YAML.load_file(config_path)
|
|
65
|
-
config = deep_merge(config, user_config)
|
|
114
|
+
def self.profile_config(profile)
|
|
115
|
+
profile_name = profile.to_s
|
|
116
|
+
raise Error, "Unknown config profile: #{profile}" unless PROFILE_OVERRIDES.key?(profile_name)
|
|
66
117
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
parent_path = File.expand_path(user_config["inherit_from"], File.dirname(config_path))
|
|
70
|
-
parent_config = YAML.load_file(parent_path) if File.exist?(parent_path)
|
|
71
|
-
config = deep_merge(parent_config, config) if parent_config
|
|
72
|
-
end
|
|
73
|
-
elsif File.exist?(".collie.yml")
|
|
74
|
-
user_config = YAML.load_file(".collie.yml")
|
|
75
|
-
config = deep_merge(config, user_config)
|
|
76
|
-
end
|
|
118
|
+
deep_merge(DEFAULT_CONFIG, PROFILE_OVERRIDES.fetch(profile_name))
|
|
119
|
+
end
|
|
77
120
|
|
|
78
|
-
|
|
121
|
+
def self.schema
|
|
122
|
+
Schema.to_h
|
|
79
123
|
end
|
|
80
124
|
|
|
81
|
-
def deep_merge(hash1, hash2)
|
|
125
|
+
def self.deep_merge(hash1, hash2)
|
|
82
126
|
hash1.merge(hash2) do |_key, old_val, new_val|
|
|
83
127
|
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
84
128
|
deep_merge(old_val, new_val)
|
|
@@ -87,5 +131,45 @@ module Collie
|
|
|
87
131
|
end
|
|
88
132
|
end
|
|
89
133
|
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def load_config(config_path)
|
|
138
|
+
config = DEFAULT_CONFIG.dup
|
|
139
|
+
if config_path
|
|
140
|
+
raise Error, "Config file not found: #{config_path}" unless File.exist?(config_path)
|
|
141
|
+
|
|
142
|
+
path = config_path
|
|
143
|
+
else
|
|
144
|
+
path = ".collie.yml" if File.exist?(".collie.yml")
|
|
145
|
+
end
|
|
146
|
+
return config unless path
|
|
147
|
+
|
|
148
|
+
user_config = load_yaml_config(path)
|
|
149
|
+
|
|
150
|
+
if user_config["inherit_from"]
|
|
151
|
+
parent_path = File.expand_path(user_config["inherit_from"], File.dirname(path))
|
|
152
|
+
raise Error, "Inherited config file not found: #{parent_path}" unless File.exist?(parent_path)
|
|
153
|
+
|
|
154
|
+
config = deep_merge(config, load_yaml_config(parent_path))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
deep_merge(config, user_config)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def load_yaml_config(path)
|
|
161
|
+
loaded = YAML.safe_load(File.read(path), aliases: true) || {}
|
|
162
|
+
raise Error, "Config file must contain a YAML mapping: #{path}" unless loaded.is_a?(Hash)
|
|
163
|
+
|
|
164
|
+
loaded
|
|
165
|
+
rescue Psych::SyntaxError => e
|
|
166
|
+
raise Error, "Invalid config file #{path}: #{e.message}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def deep_merge(hash1, hash2)
|
|
170
|
+
self.class.deep_merge(hash1, hash2)
|
|
171
|
+
end
|
|
90
172
|
end
|
|
91
173
|
end
|
|
174
|
+
|
|
175
|
+
require_relative "config/schema"
|
|
@@ -13,26 +13,28 @@ module Collie
|
|
|
13
13
|
|
|
14
14
|
# Prologue
|
|
15
15
|
output << format_prologue(ast.prologue) if ast.prologue
|
|
16
|
+
append_blank_lines(output) if ast.prologue
|
|
16
17
|
|
|
17
18
|
# Declarations
|
|
18
19
|
output << format_declarations(ast.declarations) unless ast.declarations.empty?
|
|
20
|
+
append_blank_lines(output) unless ast.declarations.empty?
|
|
19
21
|
|
|
20
22
|
# Section separator
|
|
21
|
-
output << ""
|
|
22
23
|
output << "%%"
|
|
23
|
-
output
|
|
24
|
+
append_blank_lines(output)
|
|
24
25
|
|
|
25
26
|
# Rules
|
|
26
|
-
output << format_rules(ast.rules)
|
|
27
|
+
output << format_rules(ast.rules) unless ast.rules.empty?
|
|
27
28
|
|
|
28
29
|
# Epilogue
|
|
29
30
|
if ast.epilogue
|
|
30
|
-
output
|
|
31
|
+
append_blank_lines(output)
|
|
31
32
|
output << "%%"
|
|
32
|
-
output
|
|
33
|
+
append_blank_lines(output)
|
|
33
34
|
output << ast.epilogue.code
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
output.pop while output.last == ""
|
|
36
38
|
output.join("\n")
|
|
37
39
|
end
|
|
38
40
|
|
|
@@ -43,47 +45,44 @@ module Collie
|
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
def format_declarations(declarations)
|
|
46
|
-
grouped = declarations.group_by(&:class)
|
|
47
48
|
output = []
|
|
49
|
+
index = 0
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
output <<
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Format type declarations
|
|
56
|
-
if grouped[AST::TypeDeclaration]
|
|
57
|
-
output << format_type_declarations(grouped[AST::TypeDeclaration])
|
|
51
|
+
while index < declarations.length
|
|
52
|
+
declaration = declarations[index]
|
|
53
|
+
declarations_group = consecutive_declarations(declarations, index, declaration.class)
|
|
54
|
+
output << format_declaration_group(declarations_group)
|
|
58
55
|
output << ""
|
|
56
|
+
index += declarations_group.length
|
|
59
57
|
end
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
output << ""
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Format start declaration
|
|
68
|
-
if grouped[AST::StartDeclaration]
|
|
69
|
-
start_decl = grouped[AST::StartDeclaration].first
|
|
70
|
-
output << "%start #{start_decl.symbol}"
|
|
71
|
-
output << ""
|
|
72
|
-
end
|
|
59
|
+
output.pop while output.last == ""
|
|
60
|
+
output.join("\n")
|
|
61
|
+
end
|
|
73
62
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
output << ""
|
|
78
|
-
end
|
|
63
|
+
def consecutive_declarations(declarations, start_index, declaration_class)
|
|
64
|
+
declarations[start_index..].take_while { |declaration| declaration.is_a?(declaration_class) }
|
|
65
|
+
end
|
|
79
66
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
67
|
+
def format_declaration_group(declarations)
|
|
68
|
+
case declarations.first
|
|
69
|
+
when AST::TokenDeclaration
|
|
70
|
+
format_token_declarations(declarations)
|
|
71
|
+
when AST::TypeDeclaration
|
|
72
|
+
format_type_declarations(declarations)
|
|
73
|
+
when AST::PrecedenceDeclaration
|
|
74
|
+
format_precedence_declarations(declarations)
|
|
75
|
+
when AST::UnionDeclaration
|
|
76
|
+
format_union_declarations(declarations)
|
|
77
|
+
when AST::UnknownDeclaration
|
|
78
|
+
format_unknown_declarations(declarations)
|
|
79
|
+
when AST::StartDeclaration
|
|
80
|
+
format_start_declarations(declarations)
|
|
81
|
+
when AST::ParameterizedRule
|
|
82
|
+
format_parameterized_rule_declarations(declarations)
|
|
83
|
+
when AST::InlineRule
|
|
84
|
+
format_inline_rule_declarations(declarations)
|
|
84
85
|
end
|
|
85
|
-
|
|
86
|
-
output.join("\n")
|
|
87
86
|
end
|
|
88
87
|
|
|
89
88
|
def format_token_declarations(declarations)
|
|
@@ -98,19 +97,20 @@ module Collie
|
|
|
98
97
|
max_tag_length = declarations.map { |d| d.type_tag ? d.type_tag.length + 2 : 0 }.max || 0
|
|
99
98
|
declarations.map do |decl|
|
|
100
99
|
tag = decl.type_tag ? "<#{decl.type_tag}>" : ""
|
|
101
|
-
"%token #{tag.ljust(max_tag_length)}
|
|
100
|
+
prefix = tag.empty? ? "%token" : "%token #{tag.ljust(max_tag_length)}"
|
|
101
|
+
wrap_declaration(prefix, decl.names)
|
|
102
102
|
end.join("\n")
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
def format_token_declaration(decl)
|
|
106
106
|
tag = decl.type_tag ? " <#{decl.type_tag}>" : ""
|
|
107
|
-
"%token#{tag}
|
|
107
|
+
wrap_declaration("%token#{tag}", decl.names)
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def format_type_declarations(declarations)
|
|
111
111
|
declarations.map do |decl|
|
|
112
112
|
tag = decl.type_tag ? " <#{decl.type_tag}>" : ""
|
|
113
|
-
"%type#{tag}
|
|
113
|
+
wrap_declaration("%type#{tag}", decl.names)
|
|
114
114
|
end.join("\n")
|
|
115
115
|
end
|
|
116
116
|
|
|
@@ -125,25 +125,65 @@ module Collie
|
|
|
125
125
|
|
|
126
126
|
declarations.map do |decl|
|
|
127
127
|
directive = directive_names[decl.associativity]
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
prefix = @options.align_tokens ? directive.ljust(max_directive_length) : directive
|
|
129
|
+
wrap_declaration(prefix, decl.tokens)
|
|
130
|
+
end.join("\n")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def wrap_declaration(prefix, names)
|
|
134
|
+
return prefix if names.empty?
|
|
135
|
+
|
|
136
|
+
lines = []
|
|
137
|
+
current = prefix
|
|
138
|
+
|
|
139
|
+
names.each do |name|
|
|
140
|
+
candidate = "#{current} #{name}"
|
|
141
|
+
if current != prefix && candidate.length > @options.max_line_length
|
|
142
|
+
lines << current
|
|
143
|
+
current = "#{@options.indent}#{name}"
|
|
130
144
|
else
|
|
131
|
-
|
|
145
|
+
current = candidate
|
|
132
146
|
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
lines << current
|
|
150
|
+
lines.join("\n")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def format_union_declarations(declarations)
|
|
154
|
+
declarations.map do |decl|
|
|
155
|
+
body = decl.body.to_s
|
|
156
|
+
body.start_with?("{") ? "%union #{body}" : "%union {#{body}}"
|
|
133
157
|
end.join("\n")
|
|
134
158
|
end
|
|
135
159
|
|
|
160
|
+
def format_unknown_declarations(declarations)
|
|
161
|
+
declarations.map(&:source).join("\n")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def format_start_declarations(declarations)
|
|
165
|
+
declarations.map { |decl| "%start #{decl.symbol}" }.join("\n")
|
|
166
|
+
end
|
|
167
|
+
|
|
136
168
|
def format_parameterized_rule_declarations(declarations)
|
|
137
169
|
declarations.map do |decl|
|
|
138
|
-
params = "(#{decl.parameters.join(', ')})"
|
|
170
|
+
params = decl.parameters.empty? ? "" : "(#{decl.parameters.join(', ')})"
|
|
139
171
|
alternatives = decl.alternatives.map { |alt| format_alternative(alt) }.join(" | ")
|
|
140
|
-
"
|
|
172
|
+
separator = alternatives.start_with?(" |") ? "" : " "
|
|
173
|
+
"%rule #{decl.name}#{params}:#{separator}#{alternatives} ;"
|
|
141
174
|
end.join("\n")
|
|
142
175
|
end
|
|
143
176
|
|
|
144
177
|
def format_inline_rule_declarations(declarations)
|
|
145
178
|
declarations.map do |decl|
|
|
146
|
-
"%inline #{decl.rule}"
|
|
179
|
+
output = "%inline #{decl.rule}"
|
|
180
|
+
output += "(#{decl.parameters.join(', ')})" unless decl.parameters.empty?
|
|
181
|
+
unless decl.alternatives.empty?
|
|
182
|
+
alternatives = decl.alternatives.map { |alt| format_alternative(alt) }.join(" | ")
|
|
183
|
+
separator = alternatives.start_with?(" |") ? "" : " "
|
|
184
|
+
output += ":#{separator}#{alternatives} ;"
|
|
185
|
+
end
|
|
186
|
+
output
|
|
147
187
|
end.join("\n")
|
|
148
188
|
end
|
|
149
189
|
|
|
@@ -159,18 +199,19 @@ module Collie
|
|
|
159
199
|
end
|
|
160
200
|
|
|
161
201
|
output = [rule_header]
|
|
202
|
+
indent = @options.align_alternatives ? @options.indent : ""
|
|
162
203
|
|
|
163
204
|
rule.alternatives.each_with_index do |alt, index|
|
|
164
|
-
prefix = index.zero? ? "
|
|
205
|
+
prefix = index.zero? ? "#{indent}:" : "#{indent}|"
|
|
165
206
|
output << "#{prefix} #{format_alternative(alt)}"
|
|
166
207
|
end
|
|
167
208
|
|
|
168
|
-
output << "
|
|
209
|
+
output << "#{indent};"
|
|
169
210
|
output.join("\n")
|
|
170
211
|
end
|
|
171
212
|
|
|
172
213
|
def format_alternative(alt)
|
|
173
|
-
symbols_str = alt.symbols.map { |sym| format_symbol(sym) }.join(" ")
|
|
214
|
+
symbols_str = alt.explicit_empty ? (alt.empty_marker || "%empty") : alt.symbols.map { |sym| format_symbol(sym) }.join(" ")
|
|
174
215
|
action_str = alt.action ? " #{alt.action.code}" : ""
|
|
175
216
|
prec_str = alt.prec ? " %prec #{alt.prec}" : ""
|
|
176
217
|
|
|
@@ -191,6 +232,10 @@ module Collie
|
|
|
191
232
|
|
|
192
233
|
result
|
|
193
234
|
end
|
|
235
|
+
|
|
236
|
+
def append_blank_lines(output)
|
|
237
|
+
@options.blank_lines_around_sections.times { output << "" }
|
|
238
|
+
end
|
|
194
239
|
end
|
|
195
240
|
end
|
|
196
241
|
end
|
|
@@ -8,16 +8,28 @@ module Collie
|
|
|
8
8
|
:blank_lines_around_sections, :max_line_length
|
|
9
9
|
|
|
10
10
|
def initialize(config = {})
|
|
11
|
-
@indent_size = config
|
|
12
|
-
@align_tokens = config
|
|
13
|
-
@align_alternatives = config
|
|
14
|
-
@blank_lines_around_sections = config
|
|
15
|
-
@max_line_length = config
|
|
11
|
+
@indent_size = fetch_option(config, :indent_size, 2)
|
|
12
|
+
@align_tokens = fetch_option(config, :align_tokens, true)
|
|
13
|
+
@align_alternatives = fetch_option(config, :align_alternatives, true)
|
|
14
|
+
@blank_lines_around_sections = fetch_option(config, :blank_lines_around_sections, 1)
|
|
15
|
+
@max_line_length = fetch_option(config, :max_line_length, 120)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def indent(level = 1)
|
|
19
19
|
" " * (indent_size * level)
|
|
20
20
|
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def fetch_option(config, key, default)
|
|
25
|
+
return default unless config
|
|
26
|
+
|
|
27
|
+
string_key = key.to_s
|
|
28
|
+
return config[string_key] if config.key?(string_key)
|
|
29
|
+
return config[key] if config.key?(key)
|
|
30
|
+
|
|
31
|
+
default
|
|
32
|
+
end
|
|
21
33
|
end
|
|
22
34
|
end
|
|
23
35
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Collie
|
|
4
|
+
module Formatter
|
|
5
|
+
# Semantic signature for verifying that formatting preserved grammar structure.
|
|
6
|
+
class Signature
|
|
7
|
+
def self.build(ast)
|
|
8
|
+
new.build(ast)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def build(ast)
|
|
12
|
+
[
|
|
13
|
+
:grammar,
|
|
14
|
+
ast.prologue&.code,
|
|
15
|
+
ast.declarations.map { |declaration| declaration_signature(declaration) },
|
|
16
|
+
ast.rules.map { |rule| rule_signature(rule) },
|
|
17
|
+
ast.epilogue&.code
|
|
18
|
+
]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def declaration_signature(declaration)
|
|
24
|
+
case declaration
|
|
25
|
+
when AST::TokenDeclaration
|
|
26
|
+
[:token, declaration.type_tag, declaration.names]
|
|
27
|
+
when AST::TypeDeclaration
|
|
28
|
+
[:type, declaration.type_tag, declaration.names]
|
|
29
|
+
when AST::PrecedenceDeclaration
|
|
30
|
+
[:precedence, declaration.associativity, declaration.tokens]
|
|
31
|
+
when AST::StartDeclaration
|
|
32
|
+
[:start, declaration.symbol]
|
|
33
|
+
when AST::UnionDeclaration
|
|
34
|
+
[:union, declaration.body]
|
|
35
|
+
when AST::UnknownDeclaration
|
|
36
|
+
[:unknown, declaration.source]
|
|
37
|
+
when AST::ParameterizedRule, AST::InlineRule
|
|
38
|
+
rule_signature(declaration)
|
|
39
|
+
else
|
|
40
|
+
[declaration.class.name]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def rule_signature(rule)
|
|
45
|
+
[
|
|
46
|
+
rule.is_a?(AST::InlineRule) ? :inline_rule : :rule,
|
|
47
|
+
rule.is_a?(AST::InlineRule) ? rule.rule : rule.name,
|
|
48
|
+
rule.respond_to?(:parameters) ? rule.parameters : [],
|
|
49
|
+
rule.alternatives.map { |alternative| alternative_signature(alternative) }
|
|
50
|
+
]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def alternative_signature(alternative)
|
|
54
|
+
[
|
|
55
|
+
alternative.symbols.map { |symbol| symbol_signature(symbol) },
|
|
56
|
+
alternative.action&.code,
|
|
57
|
+
alternative.prec,
|
|
58
|
+
alternative.explicit_empty
|
|
59
|
+
]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def symbol_signature(symbol)
|
|
63
|
+
[
|
|
64
|
+
symbol.name,
|
|
65
|
+
symbol.kind,
|
|
66
|
+
symbol.alias_name,
|
|
67
|
+
Array(symbol.arguments).map { |argument| symbol_signature(argument) }
|
|
68
|
+
]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/collie/linter/base.rb
CHANGED
|
@@ -28,6 +28,8 @@ module Collie
|
|
|
28
28
|
|
|
29
29
|
# Base class for all lint rules
|
|
30
30
|
class Base
|
|
31
|
+
VALID_SEVERITIES = %i[error warning convention info].freeze
|
|
32
|
+
|
|
31
33
|
class << self
|
|
32
34
|
attr_accessor :rule_name, :description, :severity, :autocorrectable
|
|
33
35
|
end
|
|
@@ -52,11 +54,58 @@ module Collie
|
|
|
52
54
|
rule: self.class,
|
|
53
55
|
location: node.location,
|
|
54
56
|
message: message,
|
|
57
|
+
severity: configured_severity,
|
|
55
58
|
autocorrect: autocorrect
|
|
56
59
|
)
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
attr_reader :offenses
|
|
63
|
+
|
|
64
|
+
def configured_severity
|
|
65
|
+
severity = config_value(:severity)
|
|
66
|
+
return self.class.severity unless severity
|
|
67
|
+
|
|
68
|
+
normalized = severity.to_sym
|
|
69
|
+
VALID_SEVERITIES.include?(normalized) ? normalized : self.class.severity
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def config_value(key, default = nil)
|
|
73
|
+
string_key = key.to_s
|
|
74
|
+
return @config[string_key] if @config.key?(string_key)
|
|
75
|
+
return @config[key] if @config.key?(key)
|
|
76
|
+
|
|
77
|
+
default
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def each_rule_like(ast)
|
|
81
|
+
ast.rules.each { |rule| yield rule }
|
|
82
|
+
|
|
83
|
+
ast.declarations.each do |declaration|
|
|
84
|
+
next unless rule_like_declaration?(declaration)
|
|
85
|
+
|
|
86
|
+
yield declaration
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def rule_like_declaration?(declaration)
|
|
91
|
+
declaration.is_a?(AST::ParameterizedRule) || declaration.is_a?(AST::InlineRule)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def rule_like_name(rule)
|
|
95
|
+
rule.is_a?(AST::InlineRule) ? rule.rule : rule.name
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def rule_like_parameters(rule)
|
|
99
|
+
rule.respond_to?(:parameters) ? rule.parameters : []
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def find_rule_like(ast, name)
|
|
103
|
+
each_rule_like(ast) do |rule|
|
|
104
|
+
return rule if rule_like_name(rule) == name
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
60
109
|
end
|
|
61
110
|
end
|
|
62
111
|
end
|