Skip to content

Commit 928d849

Browse files
committed
parser: Add unused var check
Add unused var check reporting an error if declared variable is never used. This feature was yet again borrowed from go and piggy backed on the scope data structure.
1 parent 2bd769a commit 928d849

File tree

3 files changed

+150
-17
lines changed

3 files changed

+150
-17
lines changed

pkg/parser/ast.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,10 @@ type EventHandler struct {
8181
}
8282

8383
type Var struct {
84-
Token *lexer.Token
85-
Name string
86-
T *Type
84+
Token *lexer.Token
85+
Name string
86+
T *Type
87+
isUsed bool
8788
}
8889

8990
type BlockStatement struct {

pkg/parser/parser.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ func (p *Parser) parseProgram(scope *scope) *Program {
115115
program.Statements = append(program.Statements, stmt)
116116
}
117117
}
118+
p.validateScope(scope)
118119
return program
119120
}
120121

@@ -222,7 +223,6 @@ func (p *Parser) parseAssignmentStatement(scope *scope) Node {
222223
p.advancePastNL()
223224
return nil
224225
}
225-
226226
target := p.parseAssignable(scope)
227227
tok := p.cur
228228
if target == nil {
@@ -390,7 +390,11 @@ func (p *Parser) parseTerm(scope *scope) Node {
390390
p.advance()
391391
return nil
392392
}
393-
return p.parseAssignable(scope)
393+
assignable := p.parseAssignable(scope)
394+
if v, ok := assignable.(*Var); ok {
395+
v.isUsed = true
396+
}
397+
return assignable
394398
}
395399
if p.isLiteral() {
396400
lit := p.parseLiteral(scope)
@@ -518,6 +522,15 @@ func (p *Parser) appendErrorForToken(message string, token *lexer.Token) {
518522
p.errors = append(p.errors, Error{message: message, token: token})
519523
}
520524

525+
// validateScope ensures all variables in scope have been used.
526+
func (p *Parser) validateScope(scope *scope) {
527+
for _, v := range scope.vars {
528+
if !v.isUsed {
529+
p.appendErrorForToken("'"+v.Name+"' declared but not used", v.Token)
530+
}
531+
}
532+
}
533+
521534
func (p *Parser) parseBlock(scope *scope) *BlockStatement {
522535
endTokens := map[lexer.TokenType]bool{lexer.END: true, lexer.EOF: true}
523536
return p.parseBlockWithEndTokens(scope, endTokens)
@@ -548,6 +561,7 @@ func (p *Parser) parseBlockWithEndTokens(scope *scope, endTokens map[lexer.Token
548561
if len(block.Statements) == 0 {
549562
p.appendErrorForToken("at least one statement is required here", block.Token)
550563
}
564+
p.validateScope(scope)
551565
return block
552566
}
553567

pkg/parser/parser_test.go

Lines changed: 130 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import (
1010
func TestParseDeclaration(t *testing.T) {
1111
tests := map[string][]string{
1212
"a := 1": {"a=1"},
13-
"b:bool": {"b=false"},
14-
"\nb:bool\n": {"b=false"},
13+
"a:bool": {"a=false"},
14+
"\na:bool\n": {"a=false"},
1515
`a := "abc"
1616
b:bool
17-
c := true`: {"a='abc'", "b=false", "c=true"},
17+
c := true
18+
print a b c`: {"a='abc'", "b=false", "c=true", "print(a, b, c)"},
1819
"a:num[]": {"a=[]"},
1920
"a:num[]{}": {"a={}"},
20-
"abc:any[]{}": {"abc={}"},
21+
"a:any[]{}": {"a={}"},
2122
"a := bool[true]": {"a=[true]"},
2223
"a := num[]": {"a=[]"},
2324
"a := num[][num[1 2]num[3 4]]": {"a=[[1, 2], [3, 4]]"},
@@ -32,6 +33,8 @@ func TestParseDeclaration(t *testing.T) {
3233
"a := num{}[num{a:1}]": {"a=[{a:1}]"},
3334
}
3435
for input, wantSlice := range tests {
36+
input += "\n print a"
37+
wantSlice = append(wantSlice, "print(a)")
3538
want := strings.Join(wantSlice, "\n") + "\n"
3639
parser := New(input, testBuiltins())
3740
got := parser.Parse()
@@ -91,14 +94,14 @@ func TestParseDeclarationError(t *testing.T) {
9194

9295
func TestFunctionCall(t *testing.T) {
9396
tests := map[string][]string{
94-
"print": {"print()"},
95-
"print 123": {"print(123)"},
96-
`print 123 "abc"`: {"print(123, 'abc')"},
97-
"a:=1 \n print a": {"a=1", "print(a)"},
98-
`a := len "abc"`: {"a=len('abc')"},
99-
`len "abc"`: {"len('abc')"},
100-
`len num[]`: {"len([])"},
101-
"a:string \n print a": {"a=''", "print(a)"},
97+
"print": {"print()"},
98+
"print 123": {"print(123)"},
99+
`print 123 "abc"`: {"print(123, 'abc')"},
100+
"a:=1 \n print a": {"a=1", "print(a)"},
101+
`a := len "abc"` + " \n print a": {"a=len('abc')", "print(a)"},
102+
`len "abc"`: {"len('abc')"},
103+
`len num[]`: {"len([])"},
104+
"a:string \n print a": {"a=''", "print(a)"},
102105
`a:=true
103106
b:string
104107
print a b`: {"a=true", "b=''", "print(a, b)"},
@@ -177,12 +180,14 @@ print('TRUE')
177180
func TestToplevelExprFuncCall(t *testing.T) {
178181
input := `
179182
x := len "123"
183+
print x
180184
`
181185
parser := New(input, testBuiltins())
182186
got := parser.Parse()
183187
assertNoParseError(t, parser, input)
184188
want := `
185189
x=len('123')
190+
print(x)
186191
`[1:]
187192
assert.Equal(t, want, got.String())
188193
}
@@ -315,14 +320,17 @@ func TestFuncAssignment(t *testing.T) {
315320
a := 1
316321
b:num
317322
b = a
323+
print b
318324
`, `
319325
a:num
320326
b:num
321327
b = a
328+
print b
322329
`, `
323330
a:num
324331
b:any
325332
b = a
333+
print b
326334
`,
327335
}
328336
for _, input := range inputs {
@@ -376,22 +384,35 @@ func TestScope(t *testing.T) {
376384
x := 1
377385
func foo
378386
x := "abc"
387+
print x
379388
end
389+
print x
380390
`, `
381391
x := 1
382392
func foo x:string
383393
x = "abc"
394+
print x
395+
end
396+
print x
397+
`, `
398+
x := 1
399+
func foo
400+
x = 2
401+
print x
384402
end
385403
`, `
386404
x := 1
387405
func foo x:string...
388406
print x
389407
end
408+
print x
390409
`, `
391410
x := 1
392411
if true
393412
x := "abc" // block scope
413+
print x
394414
end
415+
print x
395416
`,
396417
}
397418
for _, input := range inputs {
@@ -401,6 +422,103 @@ end
401422
}
402423
}
403424

425+
func TestUnusedErr(t *testing.T) {
426+
inputs := map[string]string{
427+
`
428+
x := 1
429+
`: "line 2 column 1: 'x' declared but not used",
430+
`
431+
x := 1
432+
if true
433+
x := 1
434+
end
435+
print x
436+
`: "line 4 column 2: 'x' declared but not used",
437+
`
438+
x := 1
439+
if true
440+
x := 1
441+
print x
442+
end
443+
`: "line 2 column 1: 'x' declared but not used",
444+
`
445+
x := 1
446+
if true
447+
print "foo"
448+
else
449+
x := 1
450+
print x
451+
end
452+
`: "line 2 column 1: 'x' declared but not used",
453+
`
454+
x := 1
455+
if true
456+
print "foo"
457+
else
458+
x := 1
459+
end
460+
print x
461+
`: "line 6 column 2: 'x' declared but not used",
462+
`
463+
x := 1
464+
if true
465+
print "foo"
466+
else if true
467+
x := 1
468+
end
469+
print x
470+
`: "line 6 column 2: 'x' declared but not used",
471+
`
472+
x := 1
473+
for i := range 10
474+
x := 2
475+
print x
476+
end
477+
`: "line 2 column 1: 'x' declared but not used",
478+
`
479+
x := 1
480+
for i := range 10
481+
x := 2
482+
end
483+
print x
484+
`: "line 4 column 2: 'x' declared but not used",
485+
`
486+
x := 1
487+
while true
488+
x := 2
489+
print x
490+
end
491+
`: "line 2 column 1: 'x' declared but not used",
492+
`
493+
x := 1
494+
while true
495+
x := 2
496+
end
497+
print x
498+
`: "line 4 column 2: 'x' declared but not used",
499+
`
500+
x := 1
501+
func foo
502+
x := 2
503+
end
504+
print x
505+
`: "line 4 column 2: 'x' declared but not used",
506+
`
507+
x := 1
508+
func foo
509+
x := 2
510+
print x
511+
end
512+
`: "line 2 column 1: 'x' declared but not used",
513+
}
514+
for input, wantErr := range inputs {
515+
parser := New(input, testBuiltins())
516+
_ = parser.Parse()
517+
assertParseError(t, parser, input)
518+
assert.Equal(t, wantErr, parser.MaxErrorsString(1))
519+
}
520+
}
521+
404522
func TestScopeErr(t *testing.T) {
405523
inputs := map[string]string{
406524
`

0 commit comments

Comments
 (0)