Skip to content

Commit 78049a5

Browse files
authored
Merge pull request #760 from ydah/warn1
Add warning for parameterized rule name conflicts with symbols
2 parents b81ea89 + bcc9790 commit 78049a5

File tree

4 files changed

+299
-0
lines changed

4 files changed

+299
-0
lines changed

lib/lrama/warnings.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
require_relative 'warnings/conflicts'
55
require_relative 'warnings/implicit_empty'
6+
require_relative 'warnings/name_conflicts'
67
require_relative 'warnings/redefined_rules'
78
require_relative 'warnings/required'
89
require_relative 'warnings/useless_precedence'
@@ -13,6 +14,7 @@ class Warnings
1314
def initialize(logger, warnings)
1415
@conflicts = Conflicts.new(logger, warnings)
1516
@implicit_empty = ImplicitEmpty.new(logger, warnings)
17+
@name_conflicts = NameConflicts.new(logger, warnings)
1618
@redefined_rules = RedefinedRules.new(logger, warnings)
1719
@required = Required.new(logger, warnings)
1820
@useless_precedence = UselessPrecedence.new(logger, warnings)
@@ -22,6 +24,7 @@ def initialize(logger, warnings)
2224
def warn(grammar, states)
2325
@conflicts.warn(states)
2426
@implicit_empty.warn(grammar)
27+
@name_conflicts.warn(grammar)
2528
@redefined_rules.warn(grammar)
2629
@required.warn(grammar)
2730
@useless_precedence.warn(grammar, states)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# rbs_inline: enabled
2+
# frozen_string_literal: true
3+
4+
module Lrama
5+
class Warnings
6+
# Warning rationale: Parameterized rule names conflicting with symbol names
7+
# - When a %rule name is identical to a terminal or non-terminal symbol name,
8+
# it reduces grammar readability and may cause unintended behavior
9+
# - Detecting these conflicts helps improve grammar definition quality
10+
class NameConflicts
11+
# @rbs (Lrama::Logger logger, bool warnings) -> void
12+
def initialize(logger, warnings)
13+
@logger = logger
14+
@warnings = warnings
15+
end
16+
17+
# @rbs (Lrama::Grammar grammar) -> void
18+
def warn(grammar)
19+
return unless @warnings
20+
return if grammar.parameterized_rules.empty?
21+
22+
symbol_names = collect_symbol_names(grammar)
23+
check_conflicts(grammar.parameterized_rules, symbol_names)
24+
end
25+
26+
private
27+
28+
# @rbs (Lrama::Grammar grammar) -> Set[String]
29+
def collect_symbol_names(grammar)
30+
symbol_names = Set.new
31+
32+
collect_term_names(grammar.terms, symbol_names)
33+
collect_nterm_names(grammar.nterms, symbol_names)
34+
35+
symbol_names
36+
end
37+
38+
# @rbs (Array[untyped] terms, Set[String] symbol_names) -> void
39+
def collect_term_names(terms, symbol_names)
40+
terms.each do |term|
41+
symbol_names.add(term.id.s_value)
42+
symbol_names.add(term.alias_name) if term.alias_name
43+
end
44+
end
45+
46+
# @rbs (Array[untyped] nterms, Set[String] symbol_names) -> void
47+
def collect_nterm_names(nterms, symbol_names)
48+
nterms.each do |nterm|
49+
symbol_names.add(nterm.id.s_value)
50+
end
51+
end
52+
53+
# @rbs (Array[untyped] parameterized_rules, Set[String] symbol_names) -> void
54+
def check_conflicts(parameterized_rules, symbol_names)
55+
parameterized_rules.each do |param_rule|
56+
next unless symbol_names.include?(param_rule.name)
57+
58+
@logger.warn("warning: parameterized rule name \"#{param_rule.name}\" conflicts with symbol name")
59+
end
60+
end
61+
end
62+
end
63+
end

sig/generated/lrama/warnings/name_conflicts.rbs

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe Lrama::Warnings::NameConflicts do
4+
describe "#warn" do
5+
context "when parameterized rule name conflicts with terminal symbol" do
6+
let(:y) do
7+
<<~STR
8+
%{
9+
// Prologue
10+
%}
11+
%union {
12+
int i;
13+
}
14+
%token <i> option tNUMBER
15+
%rule option(X) : /* empty */
16+
| X
17+
;
18+
%%
19+
program: option(tNUMBER)
20+
;
21+
STR
22+
end
23+
24+
context "when warnings true" do
25+
it "warns about the conflict" do
26+
grammar = Lrama::Parser.new(y, "name_conflicts/term_conflict.y").parse
27+
grammar.prepare
28+
grammar.validate!
29+
states = Lrama::States.new(grammar, Lrama::Tracer.new(Lrama::Logger.new))
30+
states.compute
31+
logger = Lrama::Logger.new
32+
allow(logger).to receive(:warn)
33+
Lrama::Warnings.new(logger, true).warn(grammar, states)
34+
expect(logger).to have_received(:warn).with('warning: parameterized rule name "option" conflicts with symbol name')
35+
end
36+
end
37+
38+
context "when warnings false" do
39+
it "does not warn" do
40+
grammar = Lrama::Parser.new(y, "name_conflicts/term_conflict.y").parse
41+
grammar.prepare
42+
grammar.validate!
43+
states = Lrama::States.new(grammar, Lrama::Tracer.new(Lrama::Logger.new))
44+
states.compute
45+
logger = Lrama::Logger.new
46+
allow(logger).to receive(:warn)
47+
Lrama::Warnings.new(logger, false).warn(grammar, states)
48+
expect(logger).not_to have_received(:warn)
49+
end
50+
end
51+
end
52+
53+
context "when parameterized rule name conflicts with non-terminal symbol" do
54+
let(:y) do
55+
<<~STR
56+
%{
57+
// Prologue
58+
%}
59+
%union {
60+
int i;
61+
}
62+
%token <i> tNUMBER
63+
%rule program(X) : X
64+
;
65+
%%
66+
program: tNUMBER
67+
;
68+
STR
69+
end
70+
71+
context "when warnings true" do
72+
it "warns about the conflict" do
73+
grammar = Lrama::Parser.new(y, "name_conflicts/nterm_conflict.y").parse
74+
grammar.prepare
75+
grammar.validate!
76+
states = Lrama::States.new(grammar, Lrama::Tracer.new(Lrama::Logger.new))
77+
states.compute
78+
logger = Lrama::Logger.new
79+
allow(logger).to receive(:warn)
80+
Lrama::Warnings.new(logger, true).warn(grammar, states)
81+
expect(logger).to have_received(:warn).with('warning: parameterized rule name "program" conflicts with symbol name')
82+
end
83+
end
84+
85+
context "when warnings false" do
86+
it "does not warn" do
87+
grammar = Lrama::Parser.new(y, "name_conflicts/nterm_conflict.y").parse
88+
grammar.prepare
89+
grammar.validate!
90+
states = Lrama::States.new(grammar, Lrama::Tracer.new(Lrama::Logger.new))
91+
states.compute
92+
logger = Lrama::Logger.new
93+
allow(logger).to receive(:warn)
94+
Lrama::Warnings.new(logger, false).warn(grammar, states)
95+
expect(logger).not_to have_received(:warn)
96+
end
97+
end
98+
end
99+
100+
context "when there are no conflicts" do
101+
let(:y) do
102+
<<~STR
103+
%{
104+
// Prologue
105+
%}
106+
%union {
107+
int i;
108+
}
109+
%token <i> tNUMBER
110+
%rule my_option(X) : /* empty */
111+
| X
112+
;
113+
%%
114+
program: my_option(tNUMBER)
115+
;
116+
STR
117+
end
118+
119+
context "when warnings true" do
120+
it "does not warn" do
121+
grammar = Lrama::Parser.new(y, "name_conflicts/no_conflict.y").parse
122+
grammar.prepare
123+
grammar.validate!
124+
states = Lrama::States.new(grammar, Lrama::Tracer.new(Lrama::Logger.new))
125+
states.compute
126+
logger = Lrama::Logger.new
127+
allow(logger).to receive(:warn)
128+
Lrama::Warnings.new(logger, true).warn(grammar, states)
129+
expect(logger).not_to have_received(:warn).with(/parameterized rule name.*conflicts/)
130+
end
131+
end
132+
end
133+
134+
context "when there are no parameterized rules" do
135+
let(:y) do
136+
<<~STR
137+
%{
138+
// Prologue
139+
%}
140+
%union {
141+
int i;
142+
}
143+
%token <i> tNUMBER
144+
%%
145+
program: tNUMBER
146+
;
147+
STR
148+
end
149+
150+
context "when warnings true" do
151+
it "does not warn" do
152+
grammar = Lrama::Parser.new(y, "name_conflicts/no_param_rules.y").parse
153+
grammar.prepare
154+
grammar.validate!
155+
states = Lrama::States.new(grammar, Lrama::Tracer.new(Lrama::Logger.new))
156+
states.compute
157+
logger = Lrama::Logger.new
158+
allow(logger).to receive(:warn)
159+
Lrama::Warnings.new(logger, true).warn(grammar, states)
160+
expect(logger).not_to have_received(:warn).with(/parameterized rule name.*conflicts/)
161+
end
162+
end
163+
end
164+
165+
context "when multiple parameterized rules have conflicts" do
166+
let(:y) do
167+
<<~STR
168+
%{
169+
// Prologue
170+
%}
171+
%union {
172+
int i;
173+
}
174+
%token <i> option separated tNUMBER
175+
%rule option(X) : /* empty */
176+
| X
177+
;
178+
%rule separated(X, Y) : X Y
179+
;
180+
%%
181+
program: option(tNUMBER) separated(tNUMBER, tNUMBER)
182+
;
183+
STR
184+
end
185+
186+
context "when warnings true" do
187+
it "warns about all conflicts" do
188+
grammar = Lrama::Parser.new(y, "name_conflicts/multiple_conflicts.y").parse
189+
grammar.prepare
190+
grammar.validate!
191+
states = Lrama::States.new(grammar, Lrama::Tracer.new(Lrama::Logger.new))
192+
states.compute
193+
logger = Lrama::Logger.new
194+
allow(logger).to receive(:warn)
195+
Lrama::Warnings.new(logger, true).warn(grammar, states)
196+
expect(logger).to have_received(:warn).with('warning: parameterized rule name "option" conflicts with symbol name')
197+
expect(logger).to have_received(:warn).with('warning: parameterized rule name "separated" conflicts with symbol name')
198+
end
199+
end
200+
end
201+
end
202+
end

0 commit comments

Comments
 (0)