Repository: ysugimoto/falco
Pull request #34 (2021-11-11 19:35:52)

implement unused definition/variable linting
This PR adds more linter rules of:

1. lint global definitions (table, acl, backend, subroutine) are unused (rule: `UNUSED_DEFINITION`)
2. lint local variables are unused (rule: `UNUSED_VARIABLE`)

The above rules raise `WARNING` severity error because it's ok to work, so the rule is just used to optimize the user's VCL.

And, linting unused things should do only once in the linting process, therefore I divided expose a method of `Lint()` and the internal lining method of `lint()` with keeping backward compatibility.




Patch:
diff --git a/__generator__/template.go b/__generator__/template.go
index 9551fdb..b3de789 100644
--- a/__generator__/template.go
+++ b/__generator__/template.go
@@ -9,21 +9,6 @@ import (
 	"github.com/ysugimoto/falco/types"
 )
 
-type Variables map[string]*Object
-
-type Object struct {
-	Items map[string]*Object
-	Value *Accessor
-}
-
-type Accessor struct {
-	Get       types.Type
-	Set       types.Type
-	Unset     bool
-	Scopes    int
-	Reference string
-}
-
 func predefinedVariables() Variables {
 	return {{ .Variables }}
 }
diff --git a/context/context.go b/context/context.go
index f6dbf73..7f6436e 100644
--- a/context/context.go
+++ b/context/context.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/ysugimoto/falco/ast"
 	"github.com/ysugimoto/falco/types"
 )
 
@@ -61,6 +62,23 @@ func IsFastlySubroutine(name string) bool {
 	return fastlyReservedSubroutines[name]
 }
 
+type Variables map[string]*Object
+
+type Object struct {
+	Items  map[string]*Object
+	Value  *Accessor
+	IsUsed bool
+	Meta   *ast.Meta
+}
+
+type Accessor struct {
+	Get       types.Type
+	Set       types.Type
+	Unset     bool
+	Scopes    int
+	Reference string
+}
+
 type Context struct {
 	curMode        int
 	prevMode       int
@@ -249,7 +267,7 @@ func (c *Context) Get(name string) (types.Type, error) {
 	for _, key := range remains {
 		if v, ok := obj.Items[key]; !ok {
 			// Special case, VCL allows to Set/Get/Unset for {NAME} of any key name.
-			// If program would set to this prperty, we enables to assign with its types (may string type).
+			// If program would set to this property, we enables to assign with its types (may string type).
 			if v, ok := obj.Items["%any%"]; ok {
 				key = strings.ToLower(key)
 				obj.Items[key] = &Object{
@@ -285,6 +303,10 @@ func (c *Context) Get(name string) (types.Type, error) {
 		}
 		return types.NullType, fmt.Errorf(message)
 	}
+
+	// Mark as accessed
+	obj.IsUsed = true
+
 	return obj.Value.Get, nil
 }
 
@@ -341,10 +363,14 @@ func (c *Context) Set(name string) (types.Type, error) {
 		}
 		return types.NullType, fmt.Errorf(message)
 	}
+
+	// Mark as accessed
+	obj.IsUsed = true
+
 	return obj.Value.Set, nil
 }
 
-func (c *Context) Declare(name string, valueType types.Type) error {
+func (c *Context) Declare(name string, valueType types.Type, m *ast.Meta) error {
 	if _, err := c.Get(name); err == nil {
 		// If error is nil, variable already defined
 		return fmt.Errorf(`variable "%s" is already declared`, name)
@@ -366,6 +392,7 @@ func (c *Context) Declare(name string, valueType types.Type) error {
 			// Newly assign object
 			obj.Items[key] = &Object{
 				Items: map[string]*Object{},
+				Meta:  m,
 			}
 			obj = obj.Items[key]
 		} else {
@@ -431,6 +458,10 @@ func (c *Context) Unset(name string) error {
 		}
 		return fmt.Errorf(message)
 	}
+
+	// Mark as accessed
+	obj.IsUsed = true
+
 	return nil
 }
 
diff --git a/context/predefined.go b/context/predefined.go
index 5451601..134b93f 100644
--- a/context/predefined.go
+++ b/context/predefined.go
@@ -6,21 +6,6 @@ import (
 	"github.com/ysugimoto/falco/types"
 )
 
-type Variables map[string]*Object
-
-type Object struct {
-	Items map[string]*Object
-	Value *Accessor
-}
-
-type Accessor struct {
-	Get       types.Type
-	Set       types.Type
-	Unset     bool
-	Scopes    int
-	Reference string
-}
-
 func predefinedVariables() Variables {
 	return Variables{
 		"backend": &Object{
@@ -1399,7 +1384,7 @@ func predefinedVariables() Variables {
 							Items: map[string]*Object{},
 							Value: &Accessor{
 								Get:       types.StringType,
-								Set:       types.NeverType,
+								Set:       types.StringType,
 								Unset:     false,
 								Scopes:    RECV | ERROR | DELIVER | LOG,
 								Reference: "https://developer.fastly.com/reference/vcl/variables/client-connection/client-socket-congestion-algorithm/",
@@ -1409,7 +1394,7 @@ func predefinedVariables() Variables {
 							Items: map[string]*Object{},
 							Value: &Accessor{
 								Get:       types.IntegerType,
-								Set:       types.NeverType,
+								Set:       types.IntegerType,
 								Unset:     false,
 								Scopes:    RECV | ERROR | FETCH | DELIVER | LOG,
 								Reference: "https://developer.fastly.com/reference/vcl/variables/client-connection/client-socket-cwnd/",
diff --git a/linter/errors.go b/linter/errors.go
index cdb83e6..87c0fe9 100644
--- a/linter/errors.go
+++ b/linter/errors.go
@@ -282,3 +282,22 @@ func InvalidReturnState(m *ast.Meta, scope, state string, expects ...string) *Li
 		),
 	}
 }
+
+func UnusedDeclaration(m *ast.Meta, name, declType string) *LintError {
+	return &LintError{
+		Severity: WARNING,
+		Token:    m.Token,
+		Message: fmt.Sprintf(
+			`%s "%s" is unused in VCL program`,
+			declType, name,
+		),
+	}
+}
+
+func UnusedVariable(m *ast.Meta, name string) *LintError {
+	return &LintError{
+		Severity: WARNING,
+		Token:    m.Token,
+		Message:  fmt.Sprintf(`variable "%s" is unused`, name),
+	}
+}
diff --git a/linter/linter.go b/linter/linter.go
index b0ab1bd..e6fa7b7 100644
--- a/linter/linter.go
+++ b/linter/linter.go
@@ -32,15 +32,86 @@ func (l *Linter) Error(err error) {
 	}
 }
 
+// Expose lint function to call from external program.
+// It means this method is bootstrap, called only once.
 func (l *Linter) Lint(node ast.Node, ctx *context.Context) types.Type {
 	if ctx == nil {
 		ctx = context.New()
 	}
 
+	l.lint(node, ctx)
+
+	// After whole VCLs have been linted, check all definitions are exactly used.
+	l.lintUnusedTables(ctx)
+	l.lintUnusedAcls(ctx)
+	l.lintUnusedBackends(ctx)
+	l.lintUnusedSubroutines(ctx)
+
+	return types.NeverType
+}
+
+func (l *Linter) lintUnusedTables(ctx *context.Context) {
+	for _, t := range ctx.Tables {
+		if t.IsUsed {
+			continue
+		}
+		l.Error(UnusedDeclaration(t.Decl.GetMeta(), t.Name, "table").Match(UNUSED_DECLARATION))
+	}
+}
+
+func (l *Linter) lintUnusedAcls(ctx *context.Context) {
+	for _, a := range ctx.Acls {
+		if a.IsUsed {
+			continue
+		}
+		l.Error(UnusedDeclaration(a.Decl.GetMeta(), a.Decl.Name.Value, "acl").Match(UNUSED_DECLARATION))
+	}
+}
+
+func (l *Linter) lintUnusedBackends(ctx *context.Context) {
+	for _, b := range ctx.Backends {
+		if b.IsUsed {
+			continue
+		}
+		if b.DirectorDecl != nil {
+			l.Error(UnusedDeclaration(b.DirectorDecl.GetMeta(), b.DirectorDecl.Name.Value, "director").Match(UNUSED_DECLARATION))
+		} else {
+			l.Error(UnusedDeclaration(b.BackendDecl.GetMeta(), b.BackendDecl.Name.Value, "backend").Match(UNUSED_DECLARATION))
+		}
+	}
+}
+
+func (l *Linter) lintUnusedSubroutines(ctx *context.Context) {
+	for _, s := range ctx.Subroutines {
+		if s.IsUsed {
+			continue
+		}
+		// If subroutine name is fastly's one, it's ok to be unused
+		if context.IsFastlySubroutine(s.Decl.Name.Value) {
+			continue
+		}
+		l.Error(UnusedDeclaration(s.Decl.GetMeta(), s.Decl.Name.Value, "subroutine").Match(UNUSED_DECLARATION))
+	}
+}
+
+func (l *Linter) lintUnusedVariables(ctx *context.Context) {
+	v, ok := ctx.Variables["var"]
+	if !ok {
+		return
+	}
+
+	for k, o := range v.Items {
+		if o.IsUsed {
+			continue
+		}
+		l.Error(UnusedVariable(o.Meta, k).Match(UNUSED_VARIABLE))
+	}
+}
+
+func (l *Linter) lint(node ast.Node, ctx *context.Context) types.Type {
 	switch t := node.(type) {
 	// Root program
 	case *ast.VCL:
-
 		return l.lintVCL(t, ctx)
 	// Statements
 	case *ast.AclDeclaration:
@@ -121,7 +192,7 @@ func (l *Linter) Lint(node ast.Node, ctx *context.Context) types.Type {
 
 func (l *Linter) lintVCL(vcl *ast.VCL, ctx *context.Context) types.Type {
 	for _, stmt := range vcl.Statements {
-		l.Lint(stmt, ctx)
+		l.lint(stmt, ctx)
 	}
 	return types.NeverType
 }
@@ -213,7 +284,7 @@ func (l *Linter) lintBackendProperty(prop *ast.BackendProperty, ctx *context.Con
 			if !ok {
 				l.Error(UndefinedBackendProperty(v.Key.GetMeta(), v.Key.Value).Match(BACKEND_SYNTAX))
 			}
-			vt := l.Lint(v.Value, ctx)
+			vt := l.lint(v.Value, ctx)
 			if kt != vt {
 				l.Error(InvalidType(v.Value.GetMeta(), v.Key.Value, kt, vt).Match(BACKEND_SYNTAX))
 			}
@@ -225,7 +296,7 @@ func (l *Linter) lintBackendProperty(prop *ast.BackendProperty, ctx *context.Con
 			l.Error(UndefinedBackendProperty(prop.Key.GetMeta(), prop.Key.Value).Match(BACKEND_SYNTAX))
 			return
 		}
-		vt := l.Lint(prop.Value, ctx)
+		vt := l.lint(prop.Value, ctx)
 		if kt != vt {
 			l.Error(InvalidType(prop.Value.GetMeta(), prop.Key.Value, kt, vt).Match(BACKEND_SYNTAX))
 		}
@@ -310,7 +381,7 @@ func (l *Linter) lintDirectorProperty(decl *ast.DirectorDeclaration, ctx *contex
 						l.Error(err.Match(BACKEND_NOTFOUND))
 					}
 				} else {
-					val := l.Lint(v.Value, ctx)
+					val := l.lint(v.Value, ctx)
 					if vv != val {
 						l.Error(InvalidType(v.Value.GetMeta(), v.Key.Value, vv, val).Match(dps.Rule))
 					}
@@ -343,7 +414,7 @@ func (l *Linter) lintDirectorProperty(decl *ast.DirectorDeclaration, ctx *contex
 				).Match(dps.Rule))
 				continue
 			}
-			val := l.Lint(t.Value, ctx)
+			val := l.lint(t.Value, ctx)
 			if vv != val {
 				l.Error(InvalidType(t.Value.GetMeta(), t.Key.Value, vv, val).Match(dps.Rule))
 			}
@@ -437,7 +508,7 @@ func (l *Linter) lintTableProperty(prop *ast.TableProperty, tableType types.Type
 			l.Error(UndefinedBackend(ident.GetMeta(), ident.Value))
 		}
 	default:
-		vt := l.Lint(prop.Value, ctx)
+		vt := l.lint(prop.Value, ctx)
 		if vt != tableType {
 			l.Error(InvalidType(prop.Value.GetMeta(), prop.Key.Value, tableType, vt))
 		}
@@ -466,8 +537,12 @@ func (l *Linter) lintSubRoutineDeclaration(decl *ast.SubroutineDeclaration, ctx
 
 	cc := ctx.Scope(getSubroutineCallScope(decl))
 	// Switch context mode which corredponds to call scope and restore after linting block statements
-	defer cc.Restore()
-	l.Lint(decl.Block, cc)
+	defer func() {
+		// Lint declared variables are used
+		l.lintUnusedVariables(ctx)
+		cc.Restore()
+	}()
+	l.lint(decl.Block, cc)
 
 	// If fastly reserved subroutine name (e.g vcl_recv, vcl_fetch, etc),
 	// validate fastly specific boilerplate macro is embedded like "FASTLY recv"
@@ -502,7 +577,7 @@ func (l *Linter) lintFastlyBoilerPlateMacro(sub *ast.SubroutineDeclaration, phra
 
 func (l *Linter) lintBlockStatement(block *ast.BlockStatement, ctx *context.Context) types.Type {
 	for _, stmt := range block.Statements {
-		l.Lint(stmt, ctx)
+		l.lint(stmt, ctx)
 	}
 
 	return types.NeverType
@@ -533,7 +608,7 @@ func (l *Linter) lintDeclareStatement(stmt *ast.DeclareStatement, ctx *context.C
 		l.Error(err.Match(DECLARE_STATEMENT_INVALID_TYPE))
 	}
 
-	if err := ctx.Declare(stmt.Name.Value, vt); err != nil {
+	if err := ctx.Declare(stmt.Name.Value, vt, stmt.GetMeta()); err != nil {
 		err := &LintError{
 			Severity: ERROR,
 			Token:    stmt.Name.GetMeta().Token,
@@ -568,7 +643,7 @@ func (l *Linter) lintSetStatement(stmt *ast.SetStatement, ctx *context.Context)
 		l.Error(err.Match(OPERATOR_ASSIGNMENT))
 	}
 
-	right := l.Lint(stmt.Value, ctx)
+	right := l.lint(stmt.Value, ctx)
 
 	// Fastly has various assignment operators and required correspond types for each operator
 	// https://developer.fastly.com/reference/vcl/operators/#assignment-operators
@@ -819,7 +894,7 @@ func (l *Linter) lintIfStatement(stmt *ast.IfStatement, ctx *context.Context) ty
 		}
 		l.Error(err.Match(REGEX_MATCHED_VALUE_MAY_OVERRIDE))
 	}
-	l.Lint(stmt.Consequence, ctx)
+	l.lint(stmt.Consequence, ctx)
 
 	for _, a := range stmt.Another {
 		l.lintIfCondition(a.Condition, ctx)
@@ -831,11 +906,11 @@ func (l *Linter) lintIfStatement(stmt *ast.IfStatement, ctx *context.Context) ty
 			}
 			l.Error(err.Match(REGEX_MATCHED_VALUE_MAY_OVERRIDE))
 		}
-		l.Lint(a.Consequence, ctx)
+		l.lint(a.Consequence, ctx)
 	}
 
 	if stmt.Alternative != nil {
-		l.Lint(stmt.Alternative, ctx)
+		l.lint(stmt.Alternative, ctx)
 	}
 
 	return types.NeverType
@@ -858,7 +933,7 @@ func (l *Linter) lintIfCondition(cond ast.Expression, ctx *context.Context) {
 		l.Error(err.Match(CONDITION_LITERAL))
 	}
 
-	cc := l.Lint(cond, ctx)
+	cc := l.lint(cond, ctx)
 	// Condition expression return type must be BOOL or STRING
 	if !expectType(cc, types.StringType, types.BoolType) {
 		l.Error(&LintError{
@@ -927,7 +1002,7 @@ func (l *Linter) lintAddStatement(stmt *ast.AddStatement, ctx *context.Context)
 		l.Error(err.Match(OPERATOR_ASSIGNMENT))
 	}
 
-	right := l.Lint(stmt.Value, ctx)
+	right := l.lint(stmt.Value, ctx)
 	if left != right {
 		l.Error(InvalidType(stmt.Value.GetMeta(), stmt.Ident.Value, left, right))
 	}
@@ -948,8 +1023,11 @@ func (l *Linter) lintAddStatement(stmt *ast.AddStatement, ctx *context.Context)
 func (l *Linter) lintCallStatement(stmt *ast.CallStatement, ctx *context.Context) types.Type {
 	// Note that this linter analyze up to down,
 	// so all call target subroutine must be defined before call it.
-	if _, ok := ctx.Subroutines[stmt.Subroutine.Value]; !ok {
+	if s, ok := ctx.Subroutines[stmt.Subroutine.Value]; !ok {
 		l.Error(UndefinedSubroutine(stmt.GetMeta(), stmt.Subroutine.Value).Match(CALL_STATEMENT_SUBROUTINE_NOTFOUND))
+	} else {
+		// Mark subroutine is explicitly called
+		s.IsUsed = true
 	}
 
 	return types.NeverType
@@ -970,7 +1048,7 @@ func (l *Linter) lintErrorStatement(stmt *ast.ErrorStatement, ctx *context.Conte
 	// https://developer.fastly.com/reference/vcl/statements/error/
 	switch t := stmt.Code.(type) {
 	case *ast.Ident:
-		code := l.Lint(t, ctx)
+		code := l.lint(t, ctx)
 		if code != types.IntegerType {
 			l.Error(InvalidType(t.GetMeta(), t.Value, types.IntegerType, code))
 		}
@@ -979,7 +1057,7 @@ func (l *Linter) lintErrorStatement(stmt *ast.ErrorStatement, ctx *context.Conte
 			l.Error(ErrorCodeRange(t.GetMeta(), t.Value).Match(ERROR_STATEMENT_CODE))
 		}
 	default:
-		code := l.Lint(t, ctx)
+		code := l.lint(t, ctx)
 		l.Error(InvalidType(t.GetMeta(), "error code", types.IntegerType, code))
 	}
 
@@ -987,7 +1065,7 @@ func (l *Linter) lintErrorStatement(stmt *ast.ErrorStatement, ctx *context.Conte
 }
 
 func (l *Linter) lintLogStatement(stmt *ast.LogStatement, ctx *context.Context) types.Type {
-	l.Lint(stmt.Value, ctx)
+	l.lint(stmt.Value, ctx)
 	return types.NeverType
 }
 
@@ -1050,7 +1128,7 @@ func (l *Linter) lintSyntheticStatement(stmt *ast.SyntheticStatement, ctx *conte
 		l.Error(err.Match(SYNTHETIC_STATEMENT_SCOPE))
 	}
 
-	l.Lint(stmt.Value, ctx)
+	l.lint(stmt.Value, ctx)
 	return types.NeverType
 }
 
@@ -1059,11 +1137,18 @@ func (l *Linter) lintIdent(exp *ast.Ident, ctx *context.Context) types.Type {
 	if err != nil {
 		if _, ok := ctx.Identifiers[exp.Value]; ok {
 			return types.IDType
-		} else if _, ok := ctx.Acls[exp.Value]; ok {
+		} else if a, ok := ctx.Acls[exp.Value]; ok {
+			// mark acl is used
+			a.IsUsed = true
 			return types.AclType
-		} else if _, ok := ctx.Backends[exp.Value]; ok {
+		} else if b, ok := ctx.Backends[exp.Value]; ok {
+			// mark backend is used
+			b.IsUsed = true
+
 			return types.BackendType
-		} else if _, ok := ctx.Tables[exp.Value]; ok {
+		} else if t, ok := ctx.Tables[exp.Value]; ok {
+			// mark table is used
+			t.IsUsed = true
 			return types.TableType
 		}
 		l.Error(UndefinedVariable(exp.GetMeta(), exp.Value))
@@ -1083,7 +1168,7 @@ func (l *Linter) lintSyntheticBase64Statement(stmt *ast.SyntheticBase64Statement
 	}
 
 	// TODO: check decodable string
-	l.Lint(stmt.Value, ctx)
+	l.lint(stmt.Value, ctx)
 
 	return types.NeverType
 }
@@ -1122,7 +1207,7 @@ func (l *Linter) lintRTime(exp *ast.RTime) types.Type {
 }
 
 func (l *Linter) lintPrefixExpression(exp *ast.PrefixExpression, ctx *context.Context) types.Type {
-	right := l.Lint(exp.Right, ctx)
+	right := l.lint(exp.Right, ctx)
 	if right == types.NeverType {
 		return right
 	}
@@ -1150,17 +1235,17 @@ func (l *Linter) lintPrefixExpression(exp *ast.PrefixExpression, ctx *context.Co
 }
 
 func (l *Linter) lintGroupedExpression(exp *ast.GroupedExpression, ctx *context.Context) types.Type {
-	right := l.Lint(exp.Right, ctx)
+	right := l.lint(exp.Right, ctx)
 	return right
 }
 
 func (l *Linter) lintInfixExpression(exp *ast.InfixExpression, ctx *context.Context) types.Type {
 	// Type comparison
-	left := l.Lint(exp.Left, ctx)
+	left := l.lint(exp.Left, ctx)
 	if left == types.NeverType {
 		return left
 	}
-	right := l.Lint(exp.Right, ctx)
+	right := l.lint(exp.Right, ctx)
 	if right == types.NeverType {
 		return right
 	}
@@ -1272,7 +1357,7 @@ func (l *Linter) lintIfExpression(exp *ast.IfExpression, ctx *context.Context) t
 			Message:  "cannot use constant literal in If expression consequence",
 		})
 	}
-	left := l.Lint(exp.Consequence, ctx)
+	left := l.lint(exp.Consequence, ctx)
 
 	if isConstantExpression(exp.Alternative) {
 		l.Error(&LintError{
@@ -1281,7 +1366,7 @@ func (l *Linter) lintIfExpression(exp *ast.IfExpression, ctx *context.Context) t
 			Message:  "cannot use constant literal in If expression alternative",
 		})
 	}
-	right := l.Lint(exp.Alternative, ctx)
+	right := l.lint(exp.Alternative, ctx)
 
 	if left != right {
 		l.Error(&LintError{
@@ -1335,7 +1420,7 @@ func (l *Linter) lintFunctionCallExpression(exp *ast.FunctionCallExpression, ctx
 	}
 
 	for i, v := range argTypes {
-		arg := l.Lint(exp.Arguments[i], ctx)
+		arg := l.lint(exp.Arguments[i], ctx)
 
 		switch v {
 		case types.TimeType:
diff --git a/linter/rules.go b/linter/rules.go
index 47deae3..0d34a4f 100644
--- a/linter/rules.go
+++ b/linter/rules.go
@@ -53,6 +53,8 @@ const (
 	INCLUDE_STATEMENT_MODULE_NOT_FOUND   = "include/module-not-found"
 	INCLUDE_STATEMENT_MODULE_LOAD_FAILED = "include/module-load-failed"
 	REGEX_MATCHED_VALUE_MAY_OVERRIDE     = "regex/matched-value-override"
+	UNUSED_DECLARATION                   = "unused/declaration"
+	UNUSED_VARIABLE                      = "unused/variable"
 )
 
 var references = map[Rule]string{
diff --git a/types/types.go b/types/types.go
index c83277b..3e31d08 100644
--- a/types/types.go
+++ b/types/types.go
@@ -74,7 +74,8 @@ type VCLType interface {
 }
 
 type Acl struct {
-	Decl *ast.AclDeclaration
+	Decl   *ast.AclDeclaration
+	IsUsed bool // mark this acl is accessed at least once
 }
 
 func (a *Acl) Type() Type         { return AclType }
@@ -84,6 +85,7 @@ func (a *Acl) String() string     { return a.Decl.String() }
 type Backend struct {
 	BackendDecl  *ast.BackendDeclaration
 	DirectorDecl *ast.DirectorDeclaration
+	IsUsed       bool // mark this backend is accessed at least once
 }
 
 func (b *Backend) Type() Type { return BackendType }
@@ -192,6 +194,7 @@ type Table struct {
 	Name       string
 	ValueType  Type
 	Properties []*ast.TableProperty
+	IsUsed     bool // mark this table is accessed at least once
 }
 
 func (t *Table) Type() Type         { return TableType }
@@ -199,8 +202,9 @@ func (t *Table) Token() token.Token { return t.Decl.GetMeta().Token }
 func (t *Table) String() string     { return t.Decl.String() }
 
 type Subroutine struct {
-	Decl *ast.SubroutineDeclaration
-	Body *ast.BlockStatement
+	Decl   *ast.SubroutineDeclaration
+	Body   *ast.BlockStatement
+	IsUsed bool // mark this subroutine is called at least once
 }
 
 func (s *Subroutine) Type() Type         { return SubroutineType }


Test patch:
diff --git a/context/context_test.go b/context/context_test.go
index 3e71959..635f593 100644
--- a/context/context_test.go
+++ b/context/context_test.go
@@ -123,10 +123,10 @@ func TestContextUnset(t *testing.T) {
 func TestContextDeclare(t *testing.T) {
 	t.Run("error when variable is already declared", func(t *testing.T) {
 		c := New()
-		if err := c.Declare("var.foo", types.StringType); err != nil {
+		if err := c.Declare("var.foo", types.StringType, nil); err != nil {
 			t.Errorf("expected nil but got error: %s", err)
 		}
-		if err := c.Declare("var.foo", types.StringType); err == nil {
+		if err := c.Declare("var.foo", types.StringType, nil); err == nil {
 			t.Errorf("expected error but got nil")
 		}
 	})
diff --git a/linter/linter_test.go b/linter/linter_test.go
index 3d8f1ed..a763573 100644
--- a/linter/linter_test.go
+++ b/linter/linter_test.go
@@ -16,7 +16,7 @@ func assertNoError(t *testing.T, input string) {
 	}
 
 	l := New()
-	l.Lint(vcl, context.New())
+	l.lint(vcl, context.New())
 	if len(l.Errors) > 0 {
 		t.Errorf("Lint error: %s", l.Errors)
 	}
@@ -30,7 +30,7 @@ func assertError(t *testing.T, input string) {
 	}
 
 	l := New()
-	l.Lint(vcl, context.New())
+	l.lint(vcl, context.New())
 	if len(l.Errors) == 0 {
 		t.Errorf("Expect one lint error but empty returned")
 	}
@@ -43,7 +43,7 @@ func assertErrorWithSeverity(t *testing.T, input string, severity Severity) {
 	}
 
 	l := New()
-	l.Lint(vcl, context.New())
+	l.lint(vcl, context.New())
 	if len(l.Errors) == 0 {
 		t.Errorf("Expect one lint error but empty returned")
 	}
@@ -341,7 +341,9 @@ sub vcl_recv {
 func TestLintDeclareStatement(t *testing.T) {
 	t.Run("pass", func(t *testing.T) {
 		input := `
-sub foo {
+acl foo {}
+backend bar {}
+sub bax {
 	declare local var.item1 STRING;
 	declare local var.item2 INTEGER;
 	declare local var.item3 FLOAT;
@@ -349,6 +351,15 @@ sub foo {
 	declare local var.item5 ID;
 	declare local var.item6 ACL;
 	declare local var.item7 BACKEND;
+
+	set var.item1 = "1";
+	set var.item2 = 1;
+	set var.item3 = 1.0;
+	set var.item4 = std.ip("192.168.0.1", "192.168.2");
+	set var.item5 = always;
+	set var.item6 = foo;
+	set var.item7 = bar;
+
 }`
 		assertNoError(t, input)
 	})
@@ -366,6 +377,8 @@ sub foo {
 sub foo {
 	declare local var.item1 STRING;
 	declare local var.item1 STRING;
+
+	set var.item1 = "bar";
 }`
 		assertError(t, input)
 	})
@@ -1091,7 +1104,6 @@ func TestLintIfExpression(t *testing.T) {
 		input := `
 sub foo {
 	declare local var.S STRING;
-	declare local var.I INTEGER;
 
 	set var.S = if(req.http.Host == "example.com" && req.http.Host ~ "example", "foo", "bar");
 }`
@@ -1112,7 +1124,6 @@ sub foo {
 	t.Run("raise warning when if expression returns different type", func(t *testing.T) {
 		input := `
 sub foo {
-	declare local var.S STRING;
 	declare local var.I INTEGER;
 
 	set var.I = if(req.http.Host ~ "example", "1", var.I);
@@ -1262,3 +1273,195 @@ sub vcl_recv {
 		assertError(t, input)
 	})
 }
+
+func TestUnusedAcls(t *testing.T) {
+	t.Run("pass", func(t *testing.T) {
+		input := `
+acl foo {}
+sub bar {
+	if (client.ip ~ foo) {}
+}
+`
+		vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("unexpected parser error: %s", err)
+			t.FailNow()
+		}
+
+		l := New()
+		l.Lint(vcl, context.New())
+		if len(l.Errors) == 0 {
+			t.Errorf("Expect one lint error but empty returned")
+		}
+	})
+	t.Run("raise unused error", func(t *testing.T) {
+		input := `
+acl foo {}
+`
+		vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("unexpected parser error: %s", err)
+			t.FailNow()
+		}
+
+		l := New()
+		l.Lint(vcl, context.New())
+		if len(l.Errors) == 0 {
+			t.Errorf("Expect one lint error but empty returned")
+		}
+	})
+}
+
+func TestUnusedTables(t *testing.T) {
+	t.Run("pass", func(t *testing.T) {
+		input := `
+table foo {}
+sub bar {
+	set req.http.Foo = table.lookup(foo, "bar");
+}
+`
+		vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("unexpected parser error: %s", err)
+			t.FailNow()
+		}
+
+		l := New()
+		l.Lint(vcl, context.New())
+		if len(l.Errors) == 0 {
+			t.Errorf("Expect one lint error but empty returned")
+		}
+	})
+	t.Run("raise unused error", func(t *testing.T) {
+		input := `
+table foo {}
+`
+		vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("unexpected parser error: %s", err)
+			t.FailNow()
+		}
+
+		l := New()
+		l.Lint(vcl, context.New())
+		if len(l.Errors) == 0 {
+			t.Errorf("Expect one lint error but empty returned")
+		}
+	})
+}
+
+func TestUnusedBackend(t *testing.T) {
+	t.Run("pass", func(t *testing.T) {
+		input := `
+backend foo {}
+sub vcl_recl {
+	set req.backend = foo;
+}
+`
+		vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("unexpected parser error: %s", err)
+			t.FailNow()
+		}
+
+		l := New()
+		l.Lint(vcl, context.New())
+		if len(l.Errors) == 0 {
+			t.Errorf("Expect one lint error but empty returned")
+		}
+	})
+	t.Run("raise unused error", func(t *testing.T) {
+		input := `
+backend foo {}
+`
+		vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("unexpected parser error: %s", err)
+			t.FailNow()
+		}
+
+		l := New()
+		l.Lint(vcl, context.New())
+		if len(l.Errors) == 0 {
+			t.Errorf("Expect one lint error but empty returned")
+		}
+	})
+}
+
+func TestUnusedSubroutine(t *testing.T) {
+	t.Run("pass", func(t *testing.T) {
+		input := `
+sub foo {}
+sub vcl_recl {
+	call foo;
+}
+`
+		vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("unexpected parser error: %s", err)
+			t.FailNow()
+		}
+
+		l := New()
+		l.Lint(vcl, context.New())
+		if len(l.Errors) == 0 {
+			t.Errorf("Expect one lint error but empty returned")
+		}
+	})
+	t.Run("raise unused error", func(t *testing.T) {
+		input := `
+sub foo {}
+`
+		vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("unexpected parser error: %s", err)
+			t.FailNow()
+		}
+
+		l := New()
+		l.Lint(vcl, context.New())
+		if len(l.Errors) == 0 {
+			t.Errorf("Expect one lint error but empty returned")
+		}
+	})
+}
+
+func TestUnusedVariable(t *testing.T) {
+	t.Run("pass", func(t *testing.T) {
+		input := `
+sub vcl_recv {
+	declare local var.bar STRING;
+}
+`
+		vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("unexpected parser error: %s", err)
+			t.FailNow()
+		}
+
+		l := New()
+		l.Lint(vcl, context.New())
+		if len(l.Errors) == 0 {
+			t.Errorf("Expect one lint error but empty returned")
+		}
+	})
+	t.Run("raise unused error", func(t *testing.T) {
+		input := `
+sub vcl_recv {
+	declare local var.bar STRING;
+	set var.bar = "baz";
+}
+`
+		vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
+		if err != nil {
+			t.Errorf("unexpected parser error: %s", err)
+			t.FailNow()
+		}
+
+		l := New()
+		l.Lint(vcl, context.New())
+		if len(l.Errors) == 0 {
+			t.Errorf("Expect one lint error but empty returned")
+		}
+	})
+}