Repository: ysugimoto/falco
Pull request #72 (2022-06-07 14:16:46)

Add support for Goto declaration
This PR adds support for goto declaration and use.
(Fixing https://github.com/ysugimoto/falco/issues/68)


Patch:
diff --git a/__generator__/builtin.yml b/__generator__/builtin.yml
index 8b2b4b6..6ef0eb2 100644
--- a/__generator__/builtin.yml
+++ b/__generator__/builtin.yml
@@ -300,6 +300,10 @@ digest.time_hmac_sha512:
     - [STRING, INTEGER, INTEGER]
   return: STRING
 
+h3.alt_svc:
+  reference: "https://developer.fastly.com/reference/vcl/functions/tls-and-http/h3-alt-svc/"
+  on: [RECV, HASH, HIT, MISS, PASS, FETCH, ERROR, DELIVER, LOG]
+
 parse_time_delta:
   reference: "https://developer.fastly.com/reference/vcl/functions/date-and-time/parse-time-delta/"
   on: [RECV, HASH, HIT, MISS, PASS, FETCH, ERROR, DELIVER, LOG]
@@ -626,8 +630,8 @@ resp.tarpit:
   reference: "https://developer.fastly.com/reference/vcl/functions/miscellaneous/resp-tarpit/"
   on: [DELIVER]
   arguments:
-    - [STRING, STRING]
-    - [STRING]
+    - [INTEGER, INTEGER]
+    - [INTEGER]
 
 setcookie.delete_by_name:
   reference: "https://developer.fastly.com/reference/vcl/functions/miscellaneous/setcookie-delete-by-name/"
diff --git a/ast/goto_destination_statement.go b/ast/goto_destination_statement.go
new file mode 100644
index 0000000..a6cb887
--- /dev/null
+++ b/ast/goto_destination_statement.go
@@ -0,0 +1,23 @@
+package ast
+
+import (
+	"bytes"
+)
+
+type GotoDestinationStatement struct {
+	*Meta
+	Name *Ident
+}
+
+func (gd *GotoDestinationStatement) statement()     {}
+func (gd *GotoDestinationStatement) GetMeta() *Meta { return gd.Meta }
+func (gd *GotoDestinationStatement) String() string {
+	var buf bytes.Buffer
+
+	buf.WriteString(gd.LeadingComment())
+	buf.WriteString(indent(gd.Nest) + gd.Name.Value)
+	buf.WriteString(gd.TrailingComment())
+	buf.WriteString("\n")
+
+	return buf.String()
+}
diff --git a/ast/goto_statement.go b/ast/goto_statement.go
new file mode 100644
index 0000000..01b41b7
--- /dev/null
+++ b/ast/goto_statement.go
@@ -0,0 +1,23 @@
+package ast
+
+import (
+	"bytes"
+)
+
+type GotoStatement struct {
+	*Meta
+	Destination *Ident
+}
+
+func (g *GotoStatement) statement()     {}
+func (g *GotoStatement) GetMeta() *Meta { return g.Meta }
+func (g *GotoStatement) String() string {
+	var buf bytes.Buffer
+
+	buf.WriteString(g.LeadingComment())
+	buf.WriteString(indent(g.Nest) + "goto " + g.Destination.String() + ";")
+	buf.WriteString(g.TrailingComment())
+	buf.WriteString("\n")
+
+	return buf.String()
+}
diff --git a/context/builtin.go b/context/builtin.go
index 9b5f77a..a240329 100644
--- a/context/builtin.go
+++ b/context/builtin.go
@@ -615,6 +615,17 @@ func builtinFunctions() Functions {
 				},
 			},
 		},
+		"h3": &FunctionSpec{
+			Items: map[string]*FunctionSpec{
+				"alt_svc": &FunctionSpec{
+					Items: map[string]*FunctionSpec{},
+					Value: &BuiltinFunction{
+						Scopes:    RECV | HASH | HIT | MISS | PASS | FETCH | ERROR | DELIVER | LOG,
+						Reference: "https://developer.fastly.com/reference/vcl/functions/tls-and-http/h3-alt-svc/",
+					},
+				},
+			},
+		},
 		"http_status_matches": &FunctionSpec{
 			Items: map[string]*FunctionSpec{},
 			Value: &BuiltinFunction{
@@ -1283,8 +1294,8 @@ func builtinFunctions() Functions {
 					Items: map[string]*FunctionSpec{},
 					Value: &BuiltinFunction{
 						Arguments: [][]types.Type{
-							[]types.Type{types.StringType, types.StringType},
-							[]types.Type{types.StringType},
+							[]types.Type{types.IntegerType, types.IntegerType},
+							[]types.Type{types.IntegerType},
 						},
 						Scopes:    DELIVER,
 						Reference: "https://developer.fastly.com/reference/vcl/functions/miscellaneous/resp-tarpit/",
diff --git a/context/context.go b/context/context.go
index d175bc5..26f28dd 100644
--- a/context/context.go
+++ b/context/context.go
@@ -90,6 +90,7 @@ type Context struct {
 	Subroutines    map[string]*types.Subroutine
 	Penaltyboxes   map[string]*types.Penaltybox
 	Ratecounters   map[string]*types.Ratecounter
+	Gotos          map[string]*types.Goto
 	Identifiers    map[string]struct{}
 	functions      Functions
 	Variables      Variables
@@ -108,6 +109,7 @@ func New() *Context {
 		Subroutines:    make(map[string]*types.Subroutine),
 		Penaltyboxes:   make(map[string]*types.Penaltybox),
 		Ratecounters:   make(map[string]*types.Ratecounter),
+		Gotos:          make(map[string]*types.Goto),
 		Identifiers:    builtinIdentifiers(),
 		functions:      builtinFunctions(),
 		Variables:      predefinedVariables(),
@@ -129,6 +131,8 @@ func (c *Context) Restore() *Context {
 
 	// clear local variables
 	delete(c.Variables, "var")
+	// clear local goto definitions
+	c.Gotos = make(map[string]*types.Goto)
 
 	return c
 }
@@ -361,6 +365,19 @@ func (c *Context) AddRatecounter(name string, ratecounter *types.Ratecounter) er
 	return nil
 }
 
+func (c *Context) AddGoto(name string, newGoto *types.Goto) error {
+	// append colon to the goto name to be able to identify it when it is been used.
+	name += ":"
+
+	// check existence
+	if _, duplicated := c.Gotos[name]; duplicated {
+		return fmt.Errorf(`duplicate definition of goto "%s"`, name)
+	} else {
+		c.Gotos[name] = newGoto
+	}
+	return nil
+}
+
 func (c *Context) Get(name string) (types.Type, error) {
 	first, remains := splitName(name)
 
diff --git a/linter/errors.go b/linter/errors.go
index 3f3b87f..803dfe0 100644
--- a/linter/errors.go
+++ b/linter/errors.go
@@ -313,3 +313,19 @@ func NonEmptyRatecounterBlock(m *ast.Meta, name string) *LintError {
 		Message:  fmt.Sprintf("Ratecounter %s cannot have any properties and should be declared as an empty block", name),
 	}
 }
+
+func UndefinedGotoDestination(m *ast.Meta, name string) *LintError {
+	return &LintError{
+		Severity: ERROR,
+		Token:    m.Token,
+		Message:  fmt.Sprintf("goto destination %s is not defined", name),
+	}
+}
+
+func DuplicatedUseForGotoDestination(m *ast.Meta, name string) *LintError {
+	return &LintError{
+		Severity: ERROR,
+		Token:    m.Token,
+		Message:  fmt.Sprintf("goto destination %s has been already in use", name),
+	}
+}
diff --git a/linter/helper.go b/linter/helper.go
index b53939c..651671d 100644
--- a/linter/helper.go
+++ b/linter/helper.go
@@ -171,7 +171,7 @@ func isValidConditionExpression(cond ast.Expression) error {
 // set var.Foo = req.http.Host == "example.com";   // -> invalid, equal operator could not use in set statement
 //
 // So we'd check validity following function.
-func isValidStatmentExpression(exp ast.Expression) error {
+func isValidStatementExpression(exp ast.Expression) error {
 	switch t := exp.(type) {
 	case *ast.PrefixExpression:
 		if t.Operator == "!" {
diff --git a/linter/linter.go b/linter/linter.go
index ba03daa..a7fb52f 100644
--- a/linter/linter.go
+++ b/linter/linter.go
@@ -47,6 +47,7 @@ func (l *Linter) Lint(node ast.Node, ctx *context.Context, isMain bool) types.Ty
 		l.lintUnusedAcls(ctx)
 		l.lintUnusedBackends(ctx)
 		l.lintUnusedSubroutines(ctx)
+		l.lintUnusedGotos(ctx)
 		l.lintUnusedPenaltyboxes(ctx)
 		l.lintUnusedRatecounters(ctx)
 	}
@@ -130,6 +131,16 @@ func (l *Linter) lintUnusedVariables(ctx *context.Context) {
 	}
 }
 
+func (l *Linter) lintUnusedGotos(ctx *context.Context) {
+	for _, s := range ctx.Gotos {
+		if s.IsUsed {
+			continue
+		}
+
+		l.Error(UnusedDeclaration(s.Decl.GetMeta(), s.Decl.Destination.Value, "goto").Match(UNUSED_GOTO))
+	}
+}
+
 func (l *Linter) lint(node ast.Node, ctx *context.Context) types.Type {
 	switch t := node.(type) {
 	// Root program
@@ -188,6 +199,10 @@ func (l *Linter) lint(node ast.Node, ctx *context.Context) types.Type {
 		return l.lintSyntheticStatement(t, ctx)
 	case *ast.SyntheticBase64Statement:
 		return l.lintSyntheticBase64Statement(t, ctx)
+	case *ast.GotoStatement:
+		return l.lintGotoStatement(t, ctx)
+	case *ast.GotoDestinationStatement:
+		return l.lintGotoDestinationStatement(t, ctx)
 
 	// Expressions
 	case *ast.Ident:
@@ -679,6 +694,40 @@ func (l *Linter) lintSubRoutineDeclaration(decl *ast.SubroutineDeclaration, ctx
 	return types.NeverType
 }
 
+func (l *Linter) lintGotoStatement(stmt *ast.GotoStatement, ctx *context.Context) types.Type {
+	// validate destination name
+	if !isValidName(stmt.Destination.Value) {
+		l.Error(InvalidName(stmt.Destination.GetMeta(), stmt.Destination.Value, "goto").Match(GOTO_SYNTAX))
+	}
+
+	if err := ctx.AddGoto(stmt.Destination.Value, &types.Goto{Decl: stmt}); err != nil {
+		e := &LintError{
+			Severity: ERROR,
+			Token:    stmt.Destination.GetMeta().Token,
+			Message:  err.Error(),
+		}
+		l.Error(e.Match(GOTO_DUPLICATED))
+	}
+
+	return types.NeverType
+}
+
+func (l *Linter) lintGotoDestinationStatement(stmt *ast.GotoDestinationStatement, ctx *context.Context) types.Type {
+	if gd, ok := ctx.Gotos[stmt.Name.Value]; ok {
+		if gd.IsUsed {
+			l.Error(DuplicatedUseForGotoDestination(stmt.GetMeta(), stmt.Name.Value))
+			return types.NullType
+		}
+
+		gd.IsUsed = true
+		return types.GotoType
+	} else {
+		l.Error(UndefinedGotoDestination(stmt.GetMeta(), stmt.Name.Value))
+	}
+
+	return types.NullType
+}
+
 func (l *Linter) lintPenaltyboxDeclaration(decl *ast.PenaltyboxDeclaration) types.Type {
 	// validate penaltybox name
 	if !isValidName(decl.Name.Value) {
@@ -786,7 +835,7 @@ func (l *Linter) lintSetStatement(stmt *ast.SetStatement, ctx *context.Context)
 		l.Error(err)
 	}
 
-	if err := isValidStatmentExpression(stmt.Value); err != nil {
+	if err := isValidStatementExpression(stmt.Value); err != nil {
 		err := &LintError{
 			Severity: ERROR,
 			Token:    stmt.Value.GetMeta().Token,
@@ -962,7 +1011,7 @@ func (l *Linter) lintAddStatement(stmt *ast.AddStatement, ctx *context.Context)
 		})
 	}
 
-	if err := isValidStatmentExpression(stmt.Value); err != nil {
+	if err := isValidStatementExpression(stmt.Value); err != nil {
 		err := &LintError{
 			Severity: ERROR,
 			Token:    stmt.Value.GetMeta().Token,
@@ -1157,6 +1206,10 @@ func (l *Linter) lintIdent(exp *ast.Ident, ctx *context.Context) types.Type {
 			// mark table is used
 			t.IsUsed = true
 			return types.TableType
+		} else if t, ok := ctx.Gotos[exp.Value]; ok {
+			// mark table is used
+			t.IsUsed = true
+			return types.GotoType
 		} else if p, ok := ctx.Penaltyboxes[exp.Value]; ok {
 			// mark penaltybox is used
 			p.IsUsed = true
diff --git a/linter/rules.go b/linter/rules.go
index 0ea6ac3..2c48162 100644
--- a/linter/rules.go
+++ b/linter/rules.go
@@ -54,6 +54,8 @@ const (
 	ERROR_STATEMENT_CODE                 = "error-statement/code"
 	SYNTHETIC_STATEMENT_SCOPE            = "synthetic-statement/scope"
 	SYNTHETIC_BASE64_STATEMENT_SCOPE     = "synthetic-base64-statement/scope"
+	GOTO_DUPLICATED                      = "goto/duplicated"
+	GOTO_SYNTAX                          = "goto/syntax"
 	CONDITION_LITERAL                    = "condition/literal"
 	VALID_IP                             = "valid-ip"
 	FUNCTION_ARGUMENTS                   = "function/arguments"
@@ -63,6 +65,7 @@ const (
 	REGEX_MATCHED_VALUE_MAY_OVERRIDE     = "regex/matched-value-override"
 	UNUSED_DECLARATION                   = "unused/declaration"
 	UNUSED_VARIABLE                      = "unused/variable"
+	UNUSED_GOTO                          = "unused/goto"
 )
 
 var references = map[Rule]string{
diff --git a/parser/helper.go b/parser/helper.go
index 5e482c1..64dc506 100644
--- a/parser/helper.go
+++ b/parser/helper.go
@@ -1,6 +1,8 @@
 package parser
 
 import (
+	"strings"
+
 	"github.com/ysugimoto/falco/ast"
 	"github.com/ysugimoto/falco/token"
 )
@@ -65,3 +67,9 @@ func clearComments(m *ast.Meta) *ast.Meta {
 	mm.Trailing = ast.Comments{}
 	return &mm
 }
+
+func isGotoDestination(t token.Token) bool {
+	components := strings.Split(t.Literal, ":")
+
+	return len(components) == 2
+}
diff --git a/parser/statement_parser.go b/parser/statement_parser.go
index 863400b..cc3641f 100644
--- a/parser/statement_parser.go
+++ b/parser/statement_parser.go
@@ -98,6 +98,11 @@ func (p *Parser) parseBlockStatement() (*ast.BlockStatement, error) {
 			stmt, err = p.parseSyntheticBase64Statement()
 		case token.IF:
 			stmt, err = p.parseIfStatement()
+		case token.GOTO:
+			stmt, err = p.parseGotoStatement()
+		case token.IDENT:
+			// Could be a goto destination
+			stmt, err = p.parseGotoDestination()
 		default:
 			err = UnexpectedToken(p.peekToken)
 		}
@@ -570,3 +575,36 @@ func (p *Parser) parseAnotherIfStatement() (*ast.IfStatement, error) {
 	// cursor must be on RIGHT_BRACE
 	return stmt, nil
 }
+
+func (p *Parser) parseGotoStatement() (*ast.GotoStatement, error) {
+	stmt := &ast.GotoStatement{
+		Meta: p.curToken,
+	}
+
+	if !p.expectPeek(token.IDENT) {
+		return nil, errors.WithStack(UnexpectedToken(p.peekToken, "IDENT"))
+	}
+	stmt.Destination = p.parseIdent()
+
+	if !p.peekTokenIs(token.SEMICOLON) {
+		return nil, errors.WithStack(MissingSemicolon(p.curToken))
+	}
+	stmt.Meta.Trailing = p.trailing()
+	p.nextToken() // point to SEMICOLON
+
+	return stmt, nil
+}
+
+func (p *Parser) parseGotoDestination() (*ast.GotoDestinationStatement, error) {
+	if !isGotoDestination(p.curToken.Token) {
+		return nil, errors.WithStack(UnexpectedToken(p.peekToken, "IDENT"))
+	}
+
+	stmt := &ast.GotoDestinationStatement{
+		Meta: p.curToken,
+	}
+	stmt.Name = p.parseIdent()
+	stmt.Meta.Trailing = p.trailing()
+
+	return stmt, nil
+}
diff --git a/token/token.go b/token/token.go
index 79776b4..2cf4d39 100644
--- a/token/token.go
+++ b/token/token.go
@@ -117,6 +117,7 @@ const (
 	ELSIF            = "ELSIF"            // elsif
 	PENALTYBOX       = "PENALTYBOX"       // penaltybox
 	RATECOUNTER      = "RATECOUNTER"      // ratecounter
+	GOTO             = "GOTO"             // goto
 )
 
 var keywords = map[string]TokenType{
@@ -149,6 +150,7 @@ var keywords = map[string]TokenType{
 	"synthetic.base64": SYNTHETIC_BASE64,
 	"penaltybox":       PENALTYBOX,
 	"ratecounter":      RATECOUNTER,
+	"goto":             GOTO,
 }
 
 func LookupIdent(ident string) TokenType {
diff --git a/types/types.go b/types/types.go
index 5a9ad09..2e22312 100644
--- a/types/types.go
+++ b/types/types.go
@@ -29,6 +29,7 @@ const (
 	DirectorType    Type = 0x100000000000000
 	PenaltyboxType  Type = 0x100000000000001
 	RatecounterType Type = 0x100000000000010
+	GotoType        Type = 0x100000000000100
 )
 
 func (t Type) String() string {
@@ -69,6 +70,8 @@ func (t Type) String() string {
 		return "PENALTYBOX"
 	case RatecounterType:
 		return "RATECOUNTER"
+	case GotoType:
+		return "GOTO"
 	}
 	return "UNKNOWN"
 }
@@ -242,3 +245,12 @@ type Ratecounter struct {
 func (rc *Ratecounter) Type() Type         { return RatecounterType }
 func (rc *Ratecounter) Token() token.Token { return rc.Decl.Token }
 func (rc *Ratecounter) String() string     { return rc.Decl.String() }
+
+type Goto struct {
+	Decl   *ast.GotoStatement
+	IsUsed bool // mark this goto is called at least once
+}
+
+func (g *Goto) Type() Type         { return GotoType }
+func (g *Goto) Token() token.Token { return g.Decl.Token }
+func (g *Goto) String() string     { return g.Decl.String() }


Test patch:
diff --git a/ast/goto_destination_statement_test.go b/ast/goto_destination_statement_test.go
new file mode 100644
index 0000000..fb743bc
--- /dev/null
+++ b/ast/goto_destination_statement_test.go
@@ -0,0 +1,23 @@
+package ast
+
+import (
+	"testing"
+)
+
+func TestGotoDestinationStatement(t *testing.T) {
+	g := &GotoDestinationStatement{
+		Meta: New(T, 0, comments("// This is comment"), comments("// This is comment")),
+		Name: &Ident{
+			Meta:  New(T, 0),
+			Value: "update_and_set:",
+		},
+	}
+
+	expect := `// This is comment
+update_and_set: // This is comment
+`
+
+	if g.String() != expect {
+		t.Errorf("stringer error.\nexpect:\n%s\nactual:\n%s\n", expect, g.String())
+	}
+}
diff --git a/ast/goto_statement_test.go b/ast/goto_statement_test.go
new file mode 100644
index 0000000..34b98bc
--- /dev/null
+++ b/ast/goto_statement_test.go
@@ -0,0 +1,23 @@
+package ast
+
+import (
+	"testing"
+)
+
+func TestGotoStatement(t *testing.T) {
+	g := &GotoStatement{
+		Meta: New(T, 0, comments("// This is comment"), comments("// This is comment")),
+		Destination: &Ident{
+			Meta:  New(T, 0),
+			Value: "update_and_set",
+		},
+	}
+
+	expect := `// This is comment
+goto update_and_set; // This is comment
+`
+
+	if g.String() != expect {
+		t.Errorf("stringer error.\nexpect:\n%s\nactual:\n%s\n", expect, g.String())
+	}
+}
diff --git a/linter/linter_test.go b/linter/linter_test.go
index 5ac60b7..fe023c0 100644
--- a/linter/linter_test.go
+++ b/linter/linter_test.go
@@ -1829,3 +1829,78 @@ sub test_sub{
 		assertError(t, input)
 	})
 }
+
+func TestLintGotoStatement(t *testing.T) {
+	t.Run("pass", func(t *testing.T) {
+		input := `
+	sub foo {
+		declare local var.x INTEGER;
+		set var.x = 1;
+
+		goto set_and_update;
+
+		if (var.x == 1) {
+			set var.x = 2;
+		}
+
+		set_and_update:
+		set var.x = 3;
+	}
+	`
+
+		assertNoError(t, input)
+	})
+
+	t.Run("only one destination is allowed", func(t *testing.T) {
+		input := `
+	sub foo {
+		declare local var.x INTEGER;
+		set var.x = 1;
+
+		goto set_and_update;
+
+		if (var.x == 1) {
+			set var.x = 2;
+		}
+
+		set_and_update:
+		set var.x = 3;
+		set_and_update:
+	}
+	`
+
+		assertError(t, input)
+	})
+
+	t.Run("undefined goto destination", func(t *testing.T) {
+		input := `
+	sub foo {
+		declare local var.x INTEGER;
+		set var.x = 1;
+
+		if (var.x == 1) {
+			set var.x = 2;
+		}
+
+		set_and_update:
+		set var.x = 3;
+	}
+	`
+
+		assertError(t, input)
+	})
+
+	t.Run("goto scope should be one subroutine", func(t *testing.T) {
+		input := `
+	sub some_function {
+		goto foo;
+	}
+	
+	sub another_function {
+		foo:
+	}
+	`
+
+		assertError(t, input)
+	})
+}
diff --git a/parser/statement_parser_test.go b/parser/statement_parser_test.go
index ac0797a..69d5ff8 100644
--- a/parser/statement_parser_test.go
+++ b/parser/statement_parser_test.go
@@ -1320,3 +1320,39 @@ sub vcl_recv {
 		assert(t, vcl, expect)
 	})
 }
+
+func TestGotoStatement(t *testing.T) {
+	input := `// Goto Statement
+sub vcl_recv {
+	// Leading comment
+	goto update_and_set; // Trailing comment
+}`
+	expect := &ast.VCL{
+		Statements: []ast.Statement{
+			&ast.SubroutineDeclaration{
+				Meta: ast.New(T, 0, comments("// Goto Statement")),
+				Name: &ast.Ident{
+					Meta:  ast.New(T, 0),
+					Value: "vcl_recv",
+				},
+				Block: &ast.BlockStatement{
+					Meta: ast.New(T, 1),
+					Statements: []ast.Statement{
+						&ast.GotoStatement{
+							Meta: ast.New(T, 1, comments("// Leading comment"), comments("// Trailing comment")),
+							Destination: &ast.Ident{
+								Meta:  ast.New(T, 1),
+								Value: "update_and_set",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	vcl, err := New(lexer.NewFromString(input)).ParseVCL()
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+	assert(t, vcl, expect)
+}