Repository: ysugimoto/falco
Pull request #60 (2022-05-01 13:35:37)

Add support for Fastly Rate Limiting 
### What is this PR?
This PR introduces the ability to parse and lint the Fastly Rate Limiting feature.
(also did some cleanup)

### How does this PR do that?
Fastly Rate Limiting introduces 2 new declarations: 
- [Penatlybox](https://developer.fastly.com/reference/vcl/declarations/penaltybox/)
- [Ratecounter](https://developer.fastly.com/reference/vcl/declarations/ratecounter/)

The PR adds these 2 declarations to the parsers and adds support to lint them as well.

**Note:** The two declarations don't allow any statements inside them.

Also, Faslty has 4 builtin functions for the rate-limiting functionality as well:
- [ratelimit.check_rate](https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-check-rate/)
- [ratelimit.check_rates](https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-check-rates/)
- [ratelimit.penaltybox_add](https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-penaltybox-add/)
- [ratelimit.penaltybox_has](https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-penaltybox-has/)
- [ratelimit.ratecounter_increment](https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-ratecounter-increment/)

What can I improve/add?
- Better error message when the user adds statements inside the `penaltybox` and the `ratecounter` blocks
- More tests

ysugimoto: Fixes #59 
shadialtarsha: @ysugimoto Thank you so much for the quick review! 
I really enjoyed working on this PR and on your code! Super nice! 

I am looking forward to contribute more. 
The PR is ready I think to be merged (I cannot do it by myself as I am a first-time contributor)
ysugimoto: @shadialtarsha Thanks, I'll merge this PR and release a new version.
And, I'd like to add you to the contributor then welcome another PR when you are interested in another feature 👍 

Patch:
diff --git a/__generator__/builtin.yml b/__generator__/builtin.yml
index 2f04ed5..7016cee 100644
--- a/__generator__/builtin.yml
+++ b/__generator__/builtin.yml
@@ -1217,3 +1217,36 @@ uuid.x500:
   on: [RECV, HASH, HIT, MISS, PASS, FETCH, ERROR, DELIVER, LOG]
   return: STRING
 
+ratelimit.check_rate:
+  reference: "https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-check-rate/"
+  on: [RECV, HASH, HIT, MISS, PASS, FETCH, ERROR, DELIVER, LOG]
+  arguments:
+    - [STRING, ID, INTEGER, INTEGER, INTEGER, ID, TIME]
+  return: BOOL
+
+ratelimit.check_rates:
+  reference: "https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-check-rates/"
+  on: [RECV, HASH, HIT, MISS, PASS, FETCH, ERROR, DELIVER, LOG]
+  arguments:
+    - [STRING, ID, INTEGER, INTEGER, INTEGER, ID, INTEGER, INTEGER, INTEGER, ID, TIME]
+  return: BOOL
+
+ratelimit.penaltybox_add:
+  reference: "https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-penaltybox-add/"
+  on: [RECV, HASH, HIT, MISS, PASS, FETCH, ERROR, DELIVER, LOG]
+  arguments:
+    - [ID, STRING, RTIME]
+
+ratelimit.penaltybox_has:
+  reference: "https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-penaltybox-has/"
+  on: [RECV, HASH, HIT, MISS, PASS, FETCH, ERROR, DELIVER, LOG]
+  arguments:
+    - [ID, STRING]
+  return: BOOL
+
+ratelimit.ratecounter_increment:
+  reference: "https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-ratecounter-increment/"
+  on: [RECV, HASH, HIT, MISS, PASS, FETCH, ERROR, DELIVER, LOG]
+  arguments:
+    - [ID, STRING, INTEGER]
+  return: INTEGER
diff --git a/ast/penaltybox_declaration.go b/ast/penaltybox_declaration.go
new file mode 100644
index 0000000..62fa4ba
--- /dev/null
+++ b/ast/penaltybox_declaration.go
@@ -0,0 +1,24 @@
+package ast
+
+import "bytes"
+
+type PenaltyboxDeclaration struct {
+	*Meta
+	Name  *Ident
+	Block *BlockStatement
+}
+
+func (p *PenaltyboxDeclaration) statement()     {}
+func (p *PenaltyboxDeclaration) GetMeta() *Meta { return p.Meta }
+func (p *PenaltyboxDeclaration) String() string {
+	var buf bytes.Buffer
+
+	buf.WriteString(p.LeadingComment())
+	buf.WriteString("penaltybox ")
+	buf.WriteString(p.Name.String())
+	buf.WriteString(" " + p.Block.String())
+	buf.WriteString(p.TrailingComment())
+	buf.WriteString("\n")
+
+	return buf.String()
+}
diff --git a/ast/ratecounter_declaration.go b/ast/ratecounter_declaration.go
new file mode 100644
index 0000000..20ed028
--- /dev/null
+++ b/ast/ratecounter_declaration.go
@@ -0,0 +1,24 @@
+package ast
+
+import "bytes"
+
+type RatecounterDeclaration struct {
+	*Meta
+	Name  *Ident
+	Block *BlockStatement
+}
+
+func (r *RatecounterDeclaration) statement()     {}
+func (r *RatecounterDeclaration) GetMeta() *Meta { return r.Meta }
+func (r *RatecounterDeclaration) String() string {
+	var buf bytes.Buffer
+
+	buf.WriteString(r.LeadingComment())
+	buf.WriteString("ratecounter ")
+	buf.WriteString(r.Name.String())
+	buf.WriteString(" " + r.Block.String())
+	buf.WriteString(r.TrailingComment())
+	buf.WriteString("\n")
+
+	return buf.String()
+}
diff --git a/context/builtin.go b/context/builtin.go
index d88481f..7c7d5c2 100644
--- a/context/builtin.go
+++ b/context/builtin.go
@@ -1197,6 +1197,64 @@ func builtinFunctions() Functions {
 				Reference: "https://developer.fastly.com/reference/vcl/functions/randomness/randomstr/",
 			},
 		},
+		"ratelimit": &FunctionSpec{
+			Items: map[string]*FunctionSpec{
+				"check_rate": &FunctionSpec{
+					Items: map[string]*FunctionSpec{},
+					Value: &BuiltinFunction{
+						Return: types.BoolType,
+						Arguments: [][]types.Type{
+							[]types.Type{types.StringType, types.IDType, types.IntegerType, types.IntegerType, types.IntegerType, types.IDType, types.TimeType},
+						},
+						Scopes:    RECV | HASH | HIT | MISS | PASS | FETCH | ERROR | DELIVER | LOG,
+						Reference: "https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-check-rate/",
+					},
+				},
+				"check_rates": &FunctionSpec{
+					Items: map[string]*FunctionSpec{},
+					Value: &BuiltinFunction{
+						Return: types.BoolType,
+						Arguments: [][]types.Type{
+							[]types.Type{types.StringType, types.IDType, types.IntegerType, types.IntegerType, types.IntegerType, types.IDType, types.IntegerType, types.IntegerType, types.IntegerType, types.IDType, types.TimeType},
+						},
+						Scopes:    RECV | HASH | HIT | MISS | PASS | FETCH | ERROR | DELIVER | LOG,
+						Reference: "https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-check-rates/",
+					},
+				},
+				"penaltybox_add": &FunctionSpec{
+					Items: map[string]*FunctionSpec{},
+					Value: &BuiltinFunction{
+						Arguments: [][]types.Type{
+							[]types.Type{types.IDType, types.StringType, types.RTimeType},
+						},
+						Scopes:    RECV | HASH | HIT | MISS | PASS | FETCH | ERROR | DELIVER | LOG,
+						Reference: "https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-penaltybox-add/",
+					},
+				},
+				"penaltybox_has": &FunctionSpec{
+					Items: map[string]*FunctionSpec{},
+					Value: &BuiltinFunction{
+						Return: types.BoolType,
+						Arguments: [][]types.Type{
+							[]types.Type{types.IDType, types.StringType},
+						},
+						Scopes:    RECV | HASH | HIT | MISS | PASS | FETCH | ERROR | DELIVER | LOG,
+						Reference: "https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-penaltybox-has/",
+					},
+				},
+				"ratecounter_increment": &FunctionSpec{
+					Items: map[string]*FunctionSpec{},
+					Value: &BuiltinFunction{
+						Return: types.IntegerType,
+						Arguments: [][]types.Type{
+							[]types.Type{types.IDType, types.StringType, types.IntegerType},
+						},
+						Scopes:    RECV | HASH | HIT | MISS | PASS | FETCH | ERROR | DELIVER | LOG,
+						Reference: "https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-ratecounter-increment/",
+					},
+				},
+			},
+		},
 		"regsub": &FunctionSpec{
 			Items: map[string]*FunctionSpec{},
 			Value: &BuiltinFunction{
diff --git a/context/context.go b/context/context.go
index d516dbf..1c45cbe 100644
--- a/context/context.go
+++ b/context/context.go
@@ -87,6 +87,8 @@ type Context struct {
 	Tables         map[string]*types.Table
 	Directors      map[string]*types.Director
 	Subroutines    map[string]*types.Subroutine
+	Penaltyboxes   map[string]*types.Penaltybox
+	Ratecounters   map[string]*types.Ratecounter
 	Identifiers    map[string]struct{}
 	functions      Functions
 	Variables      Variables
@@ -101,6 +103,8 @@ func New() *Context {
 		Tables:         make(map[string]*types.Table),
 		Directors:      make(map[string]*types.Director),
 		Subroutines:    make(map[string]*types.Subroutine),
+		Penaltyboxes:   make(map[string]*types.Penaltybox),
+		Ratecounters:   make(map[string]*types.Ratecounter),
 		Identifiers:    builtinIdentifiers(),
 		functions:      builtinFunctions(),
 		Variables:      predefinedVariables(),
@@ -167,7 +171,7 @@ func (c *Context) PushRegexVariables(matchN int) error {
 
 	// If pushed count is greater than 1, variable is overridden in the subroutine statement
 	if c.RegexVariables["re.group.0"] > 1 {
-		return fmt.Errorf("Regex captured variable may override older one.")
+		return fmt.Errorf("regex captured variable may override older one")
 	}
 	return nil
 }
@@ -184,7 +188,7 @@ func (c *Context) GetRegexGroupVariable(name string) (types.Type, error) {
 func (c *Context) AddAcl(name string, acl *types.Acl) error {
 	// check existence
 	if _, duplicated := c.Acls[name]; duplicated {
-		return fmt.Errorf(`duplicate deifnition of acl "%s"`, name)
+		return fmt.Errorf(`duplicate definition of acl "%s"`, name)
 	}
 	c.Acls[name] = acl
 	return nil
@@ -193,11 +197,11 @@ func (c *Context) AddAcl(name string, acl *types.Acl) error {
 func (c *Context) AddBackend(name string, backend *types.Backend) error {
 	// check existence
 	if _, duplicated := c.Backends[name]; duplicated {
-		return fmt.Errorf(`duplicate deifnition of backend "%s"`, name)
+		return fmt.Errorf(`duplicate definition of backend "%s"`, name)
 	}
 	c.Backends[name] = backend
 
-	// Additionally, assign some backend name related predefiend variable
+	// Additionally, assign some backend name related predefined variable
 	c.Variables["backend"].Items[name] = dynamicBackend()
 	c.Variables["ratecounter"] = &Object{
 		Items: map[string]*Object{
@@ -211,7 +215,7 @@ func (c *Context) AddBackend(name string, backend *types.Backend) error {
 func (c *Context) AddTable(name string, table *types.Table) error {
 	// check existence
 	if _, duplicated := c.Tables[name]; duplicated {
-		return fmt.Errorf(`duplicate deifnition of table "%s"`, name)
+		return fmt.Errorf(`duplicate definition of table "%s"`, name)
 	}
 	c.Tables[name] = table
 	return nil
@@ -220,7 +224,7 @@ func (c *Context) AddTable(name string, table *types.Table) error {
 func (c *Context) AddDirector(name string, director *types.Director) error {
 	// check existence
 	if _, duplicated := c.Directors[name]; duplicated {
-		return fmt.Errorf(`duplicate deifnition of director "%s"`, name)
+		return fmt.Errorf(`duplicate definition of director "%s"`, name)
 	}
 	c.Directors[name] = director
 
@@ -235,7 +239,7 @@ func (c *Context) AddDirector(name string, director *types.Director) error {
 		},
 	}
 
-	// And, director target backend identifilers should be marked as used
+	// And, director target backend identifiers should be marked as used
 	for _, d := range director.Decl.Properties {
 		bo, ok := d.(*ast.DirectorBackendObject)
 		if !ok {
@@ -260,7 +264,7 @@ func (c *Context) AddSubroutine(name string, subroutine *types.Subroutine) error
 	// check existence
 	if _, duplicated := c.Subroutines[name]; duplicated {
 		if !IsFastlySubroutine(name) {
-			return fmt.Errorf(`duplicate deifnition of subroutine "%s"`, name)
+			return fmt.Errorf(`duplicate definition of subroutine "%s"`, name)
 		}
 	} else {
 		c.Subroutines[name] = subroutine
@@ -268,6 +272,26 @@ func (c *Context) AddSubroutine(name string, subroutine *types.Subroutine) error
 	return nil
 }
 
+func (c *Context) AddPenaltybox(name string, penaltybox *types.Penaltybox) error {
+	// check existence
+	if _, duplicated := c.Penaltyboxes[name]; duplicated {
+		return fmt.Errorf(`duplicate definition of penaltybox "%s"`, name)
+	} else {
+		c.Penaltyboxes[name] = penaltybox
+	}
+	return nil
+}
+
+func (c *Context) AddRatecounter(name string, ratecounter *types.Ratecounter) error {
+	// check existence
+	if _, duplicated := c.Ratecounters[name]; duplicated {
+		return fmt.Errorf(`duplicate definition of ratecounter "%s"`, name)
+	} else {
+		c.Ratecounters[name] = ratecounter
+	}
+	return nil
+}
+
 func (c *Context) Get(name string) (types.Type, error) {
 	first, remains := splitName(name)
 
@@ -305,7 +329,7 @@ func (c *Context) Get(name string) (types.Type, error) {
 	if obj == nil || obj.Value == nil {
 		return types.NullType, fmt.Errorf(`undefined variable "%s"`, name)
 	}
-	// Value exists, but uneable to access in current scope
+	// Value exists, but unable to access in current scope
 	if obj.Value.Scopes&c.curMode == 0 {
 		message := fmt.Sprintf(`variable "%s" could not access in scope of %s`, name, ScopeString(c.curMode))
 		if obj.Value.Reference != "" {
@@ -365,7 +389,7 @@ func (c *Context) Set(name string) (types.Type, error) {
 	if obj == nil || obj.Value == nil {
 		return types.NullType, fmt.Errorf(`undefined variable "%s"`, name)
 	}
-	// Value exists, but uneable to access in current scope
+	// Value exists, but unable to access in current scope
 	if obj.Value.Scopes&c.curMode == 0 {
 		message := fmt.Sprintf(`variable "%s" could not access in scope of %s`, name, ScopeString(c.curMode))
 		if obj.Value.Reference != "" {
@@ -467,7 +491,7 @@ func (c *Context) Unset(name string) error {
 	if obj == nil || obj.Value == nil {
 		return nil
 	}
-	// Value exists, but uneable to access in current scope
+	// Value exists, but unable to access in current scope
 	if obj.Value.Scopes&c.curMode == 0 {
 		message := fmt.Sprintf(`variable "%s" could not access in scope of %s`, name, ScopeString(c.curMode))
 		if obj.Value.Reference != "" {
@@ -495,12 +519,12 @@ func (c *Context) GetFunction(name string) (*BuiltinFunction, error) {
 
 	obj, ok := c.functions[first]
 	if !ok {
-		return nil, fmt.Errorf(`Function "%s" is not defined`, name)
+		return nil, fmt.Errorf(`function "%s" is not defined`, name)
 	}
 
 	for _, key := range remains {
 		if v, ok := obj.Items[key]; !ok {
-			return nil, fmt.Errorf(`Function "%s" is not defined`, name)
+			return nil, fmt.Errorf(`function "%s" is not defined`, name)
 		} else {
 			obj = v
 		}
@@ -510,7 +534,7 @@ func (c *Context) GetFunction(name string) (*BuiltinFunction, error) {
 	if obj == nil || obj.Value == nil {
 		return nil, fmt.Errorf(`"%s" is not a function`, name)
 	}
-	// Value exists, but uneable to access in current scope
+	// Value exists, but unable to access in current scope
 	if obj.Value.Scopes&c.curMode == 0 {
 		return nil, fmt.Errorf(
 			`function "%s" could not call in scope of %s\ndocument: %s`,
diff --git a/context/identifilers.go b/context/identifilers.go
index c12d435..1d043e5 100644
--- a/context/identifilers.go
+++ b/context/identifilers.go
@@ -1,8 +1,8 @@
 package context
 
-// Fastly predifned identifier list.
+// Fastly predefined identifier list.
 // we listed as possible as found in Fastly document site,
-// but perhapse there are more builtin identifiers.
+// but perhaps there are more builtin identifiers.
 func builtinIdentifiers() map[string]struct{} {
 	return map[string]struct{}{
 		// use for backend.ssl_check_cert
diff --git a/linter/errors.go b/linter/errors.go
index a7a45c4..3f3b87f 100644
--- a/linter/errors.go
+++ b/linter/errors.go
@@ -130,7 +130,7 @@ func Duplicated(m *ast.Meta, name, ident string) *LintError {
 	}
 }
 
-func AccessDenined(m *ast.Meta, name, scope string) *LintError {
+func AccessDenied(m *ast.Meta, name, scope string) *LintError {
 	return &LintError{
 		Severity: ERROR,
 		Token:    m.Token,
@@ -297,3 +297,19 @@ func UnusedVariable(m *ast.Meta, name string) *LintError {
 		Message:  fmt.Sprintf(`variable "%s" is unused`, name),
 	}
 }
+
+func NonEmptyPenaltyboxBlock(m *ast.Meta, name string) *LintError {
+	return &LintError{
+		Severity: ERROR,
+		Token:    m.Token,
+		Message:  fmt.Sprintf("Penaltybox %s cannot have any properties and should be declared as an empty block", name),
+	}
+}
+
+func NonEmptyRatecounterBlock(m *ast.Meta, name string) *LintError {
+	return &LintError{
+		Severity: ERROR,
+		Token:    m.Token,
+		Message:  fmt.Sprintf("Ratecounter %s cannot have any properties and should be declared as an empty block", name),
+	}
+}
diff --git a/linter/helper.go b/linter/helper.go
index 90a4e26..bac58ac 100644
--- a/linter/helper.go
+++ b/linter/helper.go
@@ -139,7 +139,7 @@ func isValidVariableName(name string) bool {
 
 // In VCL, could not specify literal in if condition expression.
 //
-// if (!req.http.Cookie) { ... } // -> valid, inverse actual dentity variable (implicit type conversion will occur)
+// if (!req.http.Cookie) { ... } // -> valid, inverse actual identity variable (implicit type conversion will occur)
 // if (!false) { ... }           // -> valid, inverse boolean
 // if (!10) { ... }              // -> invalid, could not use with integer literal
 // if (!"example") { ... }       // -> invalid, could not use with string literal
@@ -153,7 +153,7 @@ func isValidConditionExpression(cond ast.Expression) error {
 		return isValidConditionExpression(t.Left)
 	default:
 		if isLiteralExpression(cond) {
-			return fmt.Errorf("Could not specify literal in first expression")
+			return fmt.Errorf("could not specify literal in first expression")
 		}
 	}
 	return nil
@@ -174,11 +174,11 @@ func isValidStatmentExpression(exp ast.Expression) error {
 	switch t := exp.(type) {
 	case *ast.PrefixExpression:
 		if t.Operator == "!" {
-			return fmt.Errorf("Could not specify bang operator in first expression")
+			return fmt.Errorf("could not specify bang operator in first expression")
 		}
 	case *ast.InfixExpression:
 		if t.Operator != "+" {
-			return fmt.Errorf("Could not specify %s operator in statement", t.Operator)
+			return fmt.Errorf("could not specify %s operator in statement", t.Operator)
 		}
 	}
 	return nil
diff --git a/linter/linter.go b/linter/linter.go
index b58e4ee..ac5cc9f 100644
--- a/linter/linter.go
+++ b/linter/linter.go
@@ -128,6 +128,10 @@ func (l *Linter) lint(node ast.Node, ctx *context.Context) types.Type {
 		return l.lintTableDeclaration(t, ctx)
 	case *ast.SubroutineDeclaration:
 		return l.lintSubRoutineDeclaration(t, ctx)
+	case *ast.PenaltyboxDeclaration:
+		return l.lintPenaltyboxDeclaration(t)
+	case *ast.RatecounterDeclaration:
+		return l.lintRatecounterDeclaration(t)
 
 	// Statements
 	case *ast.BlockStatement:
@@ -191,7 +195,7 @@ func (l *Linter) lint(node ast.Node, ctx *context.Context) types.Type {
 	case *ast.FunctionCallExpression:
 		return l.lintFunctionCallExpression(t, ctx)
 	default:
-		l.Error(fmt.Errorf("Unexpected node: %s", node.String()))
+		l.Error(fmt.Errorf("unexpected node: %s", node.String()))
 	}
 	return types.NeverType
 }
@@ -287,8 +291,28 @@ func (l *Linter) factoryRootStatements(vcl *ast.VCL, ctx *context.Context) []ast
 				l.Error(e.Match(SUBROUTINE_DUPLICATED))
 			}
 			statements = append(statements, stmt)
+		case *ast.PenaltyboxDeclaration:
+			if err := ctx.AddPenaltybox(t.Name.Value, &types.Penaltybox{Penaltybox: t}); err != nil {
+				e := &LintError{
+					Severity: ERROR,
+					Token:    t.Name.GetMeta().Token,
+					Message:  err.Error(),
+				}
+				l.Error(e.Match(PENALTYBOX_DUPLICATED))
+			}
+			statements = append(statements, stmt)
+		case *ast.RatecounterDeclaration:
+			if err := ctx.AddRatecounter(t.Name.Value, &types.Ratecounter{Ratecounter: t}); err != nil {
+				e := &LintError{
+					Severity: ERROR,
+					Token:    t.Name.GetMeta().Token,
+					Message:  err.Error(),
+				}
+				l.Error(e.Match(SUBROUTINE_DUPLICATED))
+			}
+			statements = append(statements, stmt)
 		default:
-			l.Error(fmt.Errorf("Unexpected statement declaration found: %s", t.String()))
+			l.Error(fmt.Errorf("unexpected statement declaration found: %s", t.String()))
 		}
 	}
 	return statements
@@ -581,6 +605,32 @@ func (l *Linter) lintSubRoutineDeclaration(decl *ast.SubroutineDeclaration, ctx
 	return types.NeverType
 }
 
+func (l *Linter) lintPenaltyboxDeclaration(decl *ast.PenaltyboxDeclaration) types.Type {
+	// validate penaltybox name
+	if !isValidName(decl.Name.Value) {
+		l.Error(InvalidName(decl.Name.GetMeta(), decl.Name.Value, "penaltybox").Match(PENALTYBOX_SYNTAX))
+	}
+
+	if len(decl.Block.Statements) > 0 {
+		l.Error(NonEmptyPenaltyboxBlock(decl.GetMeta(), decl.Name.Value).Match(PENALTYBOX_NONEMPTY_BLOCK))
+	}
+
+	return types.NeverType
+}
+
+func (l *Linter) lintRatecounterDeclaration(decl *ast.RatecounterDeclaration) types.Type {
+	// validate ratecounter name
+	if !isValidName(decl.Name.Value) {
+		l.Error(InvalidName(decl.Name.GetMeta(), decl.Name.Value, "ratecounter").Match(RATECOUNTER_SYNTAX))
+	}
+
+	if len(decl.Block.Statements) > 0 {
+		l.Error(NonEmptyRatecounterBlock(decl.GetMeta(), decl.Name.Value).Match(RATECOUNTER_NONEMPTY_BLOCK))
+	}
+
+	return types.NeverType
+}
+
 func (l *Linter) lintFastlyBoilerPlateMacro(sub *ast.SubroutineDeclaration, phrase string) {
 	// visit all statement comments and find "FASTLY [phase]" comment
 	if hasFastlyBoilerPlateMacro(sub.Block.InfixComment(), phrase) {
@@ -683,7 +733,7 @@ func (l *Linter) lintSetStatement(stmt *ast.SetStatement, ctx *context.Context)
 	case "+=", "-=":
 		l.lintAddSubOperator(stmt.Operator, left, right, isLiteralExpression(stmt.Value))
 	case "*=", "/=", "%=":
-		l.lintArithmeticOpereator(stmt.Operator, left, right, isLiteralExpression(stmt.Value))
+		l.lintArithmeticOperator(stmt.Operator, left, right, isLiteralExpression(stmt.Value))
 	case "|=", "&=", "^=", "<<=", ">>=", "rol=", "ror=":
 		l.lintBitwiseOperator(stmt.Operator, left, right)
 	case "||=", "&&=":
@@ -1242,7 +1292,7 @@ func (l *Linter) lintFunctionCallExpression(exp *ast.FunctionCallExpression, ctx
 					exp.Function.String(), len(exp.Arguments),
 				),
 			}
-			l.Error(err.Match(FUNCTION_ARGUMENS).Ref(fn.Reference))
+			l.Error(err.Match(FUNCTION_ARGUMENTS).Ref(fn.Reference))
 		}
 		return fn.Return
 	}
@@ -1258,7 +1308,7 @@ func (l *Linter) lintFunctionCallExpression(exp *ast.FunctionCallExpression, ctx
 		l.Error(FunctionArgumentMismatch(
 			exp.Function.GetMeta(), exp.Function.String(),
 			len(fn.Arguments), len(exp.Arguments),
-		).Match(FUNCTION_ARGUMENS).Ref(fn.Reference))
+		).Match(FUNCTION_ARGUMENTS).Ref(fn.Reference))
 	}
 
 	for i, v := range argTypes {
diff --git a/linter/operator.go b/linter/operator.go
index f15384f..cff2308 100644
--- a/linter/operator.go
+++ b/linter/operator.go
@@ -149,7 +149,7 @@ func (l *Linter) lintAddSubOperator(op *ast.Operator, left, right types.Type, is
 }
 
 // Lint arithmetic operator excepts addition and subtraction. Lint "*=", "/=" and "%=" operator
-func (l *Linter) lintArithmeticOpereator(op *ast.Operator, left, right types.Type, isLiteral bool) {
+func (l *Linter) lintArithmeticOperator(op *ast.Operator, left, right types.Type, isLiteral bool) {
 	switch left {
 	case types.IntegerType:
 		switch right {
diff --git a/linter/rules.go b/linter/rules.go
index 0d34a4f..f9e3195 100644
--- a/linter/rules.go
+++ b/linter/rules.go
@@ -30,6 +30,12 @@ const (
 	SUBROUTINE_SYNTAX                    = "subroutine/syntax"
 	SUBROUTINE_BOILERPLATE_MACRO         = "subroutine/boilerplate-macro"
 	SUBROUTINE_DUPLICATED                = "subroutine/duplicated"
+	PENALTYBOX_SYNTAX                    = "penaltybox/syntax"
+	PENALTYBOX_DUPLICATED                = "penaltybox/duplicated"
+	PENALTYBOX_NONEMPTY_BLOCK            = "penaltybox/nonempty-block"
+	RATECOUNTER_SYNTAX                   = "ratecounter/syntax"
+	RATECOUNTER_DUPLICATED               = "ratecounter/duplicated"
+	RATECOUNTER_NONEMPTY_BLOCK           = "ratecounter/nonempty-block"
 	DECLARE_STATEMENT_SYNTAX             = "declare-statement/syntax"
 	DECLARE_STATEMENT_INVALID_TYPE       = "declare-statement/invalid-type"
 	DECLARE_STATEMENT_DUPLICATED         = "declare-statement/duplicated"
@@ -48,7 +54,7 @@ const (
 	SYNTHETIC_BASE64_STATEMENT_SCOPE     = "synthetic-base64-statement/scope"
 	CONDITION_LITERAL                    = "condition/literal"
 	VALID_IP                             = "valid-ip"
-	FUNCTION_ARGUMENS                    = "function/arguments"
+	FUNCTION_ARGUMENTS                   = "function/arguments"
 	FUNCTION_ARGUMENT_TYPE               = "function/argument-type"
 	INCLUDE_STATEMENT_MODULE_NOT_FOUND   = "include/module-not-found"
 	INCLUDE_STATEMENT_MODULE_LOAD_FAILED = "include/module-load-failed"
@@ -71,6 +77,10 @@ var references = map[Rule]string{
 	TABLE_ITEM_LIMITATION:            "https://developer.fastly.com/reference/vcl/declarations/table/#limitations",
 	SUBROUTINE_SYNTAX:                "https://developer.fastly.com/reference/vcl/subroutines/",
 	SUBROUTINE_BOILERPLATE_MACRO:     "https://developer.fastly.com/learning/vcl/using/#adding-vcl-to-your-service-configuration",
+	PENALTYBOX_SYNTAX:                "https://developer.fastly.com/reference/vcl/declarations/penaltybox/",
+	PENALTYBOX_NONEMPTY_BLOCK:        "https://developer.fastly.com/reference/vcl/declarations/penaltybox/",
+	RATECOUNTER_SYNTAX:               "https://developer.fastly.com/reference/vcl/declarations/ratecounter/",
+	RATECOUNTER_NONEMPTY_BLOCK:       "https://developer.fastly.com/reference/vcl/declarations/ratecounter/",
 	DECLARE_STATEMENT_SYNTAX:         "https://developer.fastly.com/reference/vcl/variables/#user-defined-variables",
 	DECLARE_STATEMENT_INVALID_TYPE:   "https://developer.fastly.com/reference/vcl/variables/#user-defined-variables",
 	SET_STATEMENT_SYNTAX:             "https://developer.fastly.com/reference/vcl/statements/set/",
diff --git a/parser/declaration_parser.go b/parser/declaration_parser.go
index d1d6602..5eeb2de 100644
--- a/parser/declaration_parser.go
+++ b/parser/declaration_parser.go
@@ -440,7 +440,53 @@ func (p *Parser) parseSubroutineDeclaration() (*ast.SubroutineDeclaration, error
 	if s.Block, err = p.parseBlockStatement(); err != nil {
 		return nil, errors.WithStack(err)
 	}
-	// After block statement is parsed, cusor should point RIGHT_BRACE, end of block statement
+	// After block statement is parsed, cursor should point to RIGHT_BRACE, end of block statement
 
 	return s, nil
 }
+
+func (p *Parser) parsePenaltyboxDeclaration() (*ast.PenaltyboxDeclaration, error) {
+	pb := &ast.PenaltyboxDeclaration{
+		Meta: p.curToken,
+	}
+
+	if !p.expectPeek(token.IDENT) {
+		return nil, errors.WithStack(UnexpectedToken(p.peekToken, "IDENT"))
+	}
+	pb.Name = p.parseIdent()
+
+	if !p.expectPeek(token.LEFT_BRACE) {
+		return nil, errors.WithStack(UnexpectedToken(p.peekToken, "LEFT_BRACE"))
+	}
+	swapLeadingTrailing(p.curToken, pb.Name.Meta)
+
+	var err error
+	if pb.Block, err = p.parseBlockStatement(); err != nil {
+		return nil, errors.WithStack(err)
+	}
+
+	return pb, nil
+}
+
+func (p *Parser) parseRatecounterDeclaration() (*ast.RatecounterDeclaration, error) {
+	r := &ast.RatecounterDeclaration{
+		Meta: p.curToken,
+	}
+
+	if !p.expectPeek(token.IDENT) {
+		return nil, errors.WithStack(UnexpectedToken(p.peekToken, "IDENT"))
+	}
+	r.Name = p.parseIdent()
+
+	if !p.expectPeek(token.LEFT_BRACE) {
+		return nil, errors.WithStack(UnexpectedToken(p.peekToken, "LEFT_BRACE"))
+	}
+	swapLeadingTrailing(p.curToken, r.Name.Meta)
+
+	var err error
+	if r.Block, err = p.parseBlockStatement(); err != nil {
+		return nil, errors.WithStack(err)
+	}
+
+	return r, nil
+}
diff --git a/parser/parser.go b/parser/parser.go
index 449325b..4dc6eef 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -187,6 +187,10 @@ func (p *Parser) parse() (ast.Statement, error) {
 		stmt, err = p.parseTableDeclaration()
 	case token.SUBROUTINE:
 		stmt, err = p.parseSubroutineDeclaration()
+	case token.PENALTYBOX:
+		stmt, err = p.parsePenaltyboxDeclaration()
+	case token.RATECOUNTER:
+		stmt, err = p.parseRatecounterDeclaration()
 	default:
 		err = UnexpectedToken(p.curToken)
 	}
diff --git a/plugin/io.go b/plugin/io.go
index 5605d60..31850f3 100644
--- a/plugin/io.go
+++ b/plugin/io.go
@@ -49,6 +49,8 @@ func init() {
 	gob.Register(&ast.SetStatement{})
 	gob.Register(&ast.String{})
 	gob.Register(&ast.SubroutineDeclaration{})
+	gob.Register(&ast.PenaltyboxDeclaration{})
+	gob.Register(&ast.RatecounterDeclaration{})
 	gob.Register(&ast.SyntheticBase64Statement{})
 	gob.Register(&ast.SyntheticStatement{})
 	gob.Register(&ast.TableDeclaration{})
diff --git a/token/token.go b/token/token.go
index e5f0637..79776b4 100644
--- a/token/token.go
+++ b/token/token.go
@@ -115,6 +115,8 @@ const (
 	ELSE             = "ELSE"             // else
 	ELSEIF           = "ELSEIF"           // elseif
 	ELSIF            = "ELSIF"            // elsif
+	PENALTYBOX       = "PENALTYBOX"       // penaltybox
+	RATECOUNTER      = "RATECOUNTER"      // ratecounter
 )
 
 var keywords = map[string]TokenType{
@@ -145,6 +147,8 @@ var keywords = map[string]TokenType{
 	"false":            FALSE,
 	"remove":           REMOVE,
 	"synthetic.base64": SYNTHETIC_BASE64,
+	"penaltybox":       PENALTYBOX,
+	"ratecounter":      RATECOUNTER,
 }
 
 func LookupIdent(ident string) TokenType {
diff --git a/types/types.go b/types/types.go
index 3e31d08..3a958a1 100644
--- a/types/types.go
+++ b/types/types.go
@@ -11,22 +11,24 @@ type Type int
 
 const (
 	// https://developer.fastly.com/reference/vcl/types/
-	NeverType      Type = 0x000000000000000
-	AclType        Type = 0x000000000000001
-	BackendType    Type = 0x000000000000010
-	BoolType       Type = 0x000000000000100
-	FloatType      Type = 0x000000000001000
-	IDType         Type = 0x000000000010000
-	IntegerType    Type = 0x000000000100000
-	IPType         Type = 0x000000001000000
-	RTimeType      Type = 0x000000010000000
-	StringType     Type = 0x000000100000000
-	TimeType       Type = 0x000001000000000
-	NullType       Type = 0x000010000000000
-	ErrorType      Type = 0x000100000000000
-	SubroutineType Type = 0x001000000000000
-	TableType      Type = 0x010000000000000
-	DirectorType   Type = 0x100000000000000
+	NeverType       Type = 0x000000000000000
+	AclType         Type = 0x000000000000001
+	BackendType     Type = 0x000000000000010
+	BoolType        Type = 0x000000000000100
+	FloatType       Type = 0x000000000001000
+	IDType          Type = 0x000000000010000
+	IntegerType     Type = 0x000000000100000
+	IPType          Type = 0x000000001000000
+	RTimeType       Type = 0x000000010000000
+	StringType      Type = 0x000000100000000
+	TimeType        Type = 0x000001000000000
+	NullType        Type = 0x000010000000000
+	ErrorType       Type = 0x000100000000000
+	SubroutineType  Type = 0x001000000000000
+	TableType       Type = 0x010000000000000
+	DirectorType    Type = 0x100000000000000
+	PenaltyboxType  Type = 0x100000000000001
+	RatecounterType Type = 0x100000000000010
 )
 
 func (t Type) String() string {
@@ -63,6 +65,10 @@ func (t Type) String() string {
 		return "TABLE"
 	case DirectorType:
 		return "DIRECTOR"
+	case PenaltyboxType:
+		return "PENALTYBOX"
+	case RatecounterType:
+		return "RATECOUNTER"
 	}
 	return "UNKNOWN"
 }
@@ -218,3 +224,19 @@ type Director struct {
 func (d *Director) Type() Type         { return DirectorType }
 func (d *Director) Token() token.Token { return d.Decl.Token }
 func (d *Director) String() string     { return d.Decl.String() }
+
+type Penaltybox struct {
+	Penaltybox *ast.PenaltyboxDeclaration
+}
+
+func (pb *Penaltybox) Type() Type         { return PenaltyboxType }
+func (pb *Penaltybox) Token() token.Token { return pb.Penaltybox.Token }
+func (pb *Penaltybox) String() string     { return pb.Penaltybox.String() }
+
+type Ratecounter struct {
+	Ratecounter *ast.RatecounterDeclaration
+}
+
+func (rc *Ratecounter) Type() Type         { return RatecounterType }
+func (rc *Ratecounter) Token() token.Token { return rc.Ratecounter.Token }
+func (rc *Ratecounter) String() string     { return rc.Ratecounter.String() }


Test patch:
diff --git a/ast/penaltybox_declaration_test.go b/ast/penaltybox_declaration_test.go
new file mode 100644
index 0000000..976cbec
--- /dev/null
+++ b/ast/penaltybox_declaration_test.go
@@ -0,0 +1,27 @@
+package ast
+
+import (
+	"testing"
+)
+
+func TestPenaltyboxStatement(t *testing.T) {
+	p := &PenaltyboxDeclaration{
+		Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */")),
+		Name: &Ident{
+			Meta:  New(T, 0),
+			Value: "ip_pbox",
+		},
+		Block: &BlockStatement{
+			Meta: New(T, 0, comments("/* This is comment */")),
+		},
+	}
+
+	expect := `// This is comment
+penaltybox ip_pbox {
+} /* This is comment */
+`
+
+	if p.String() != expect {
+		t.Errorf("stringer error.\nexpect:\n%s\nactual:\n%s\n", expect, p.String())
+	}
+}
diff --git a/ast/ratecounter_declaration_test.go b/ast/ratecounter_declaration_test.go
new file mode 100644
index 0000000..be8cf99
--- /dev/null
+++ b/ast/ratecounter_declaration_test.go
@@ -0,0 +1,27 @@
+package ast
+
+import (
+	"testing"
+)
+
+func TestRatecounterStatement(t *testing.T) {
+	r := &RatecounterDeclaration{
+		Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */")),
+		Name: &Ident{
+			Meta:  New(T, 0),
+			Value: "requests_rate",
+		},
+		Block: &BlockStatement{
+			Meta: New(T, 0, comments("/* This is comment */")),
+		},
+	}
+
+	expect := `// This is comment
+ratecounter requests_rate {
+} /* This is comment */
+`
+
+	if r.String() != expect {
+		t.Errorf("stringer error.\nexpect:\n%s\nactual:\n%s\n", expect, r.String())
+	}
+}
diff --git a/linter/linter_test.go b/linter/linter_test.go
index a0a9e78..bc0a5b2 100644
--- a/linter/linter_test.go
+++ b/linter/linter_test.go
@@ -335,7 +335,6 @@ sub vcl_recv {
 }`
 		assertError(t, input)
 	})
-
 }
 
 func TestLintDeclareStatement(t *testing.T) {
@@ -438,7 +437,6 @@ sub foo {
 
 		assertError(t, input)
 	})
-
 }
 
 func TestLintUnsetStatement(t *testing.T) {
@@ -477,7 +475,6 @@ sub foo {
 
 		assertError(t, input)
 	})
-
 }
 
 func TestLintAddStatement(t *testing.T) {
@@ -590,7 +587,6 @@ sub foo {
 `
 		assertError(t, input)
 	})
-
 }
 
 func TestLintIfStatement(t *testing.T) {
@@ -1507,3 +1503,87 @@ sub hoisted_subroutine {
 		assertNoError(t, input)
 	})
 }
+
+func TestLintPenaltyboxStatement(t *testing.T) {
+	t.Run("pass", func(t *testing.T) {
+		input := `
+penaltybox ip_pb {}
+`
+		assertNoError(t, input)
+	})
+
+	t.Run("pass with comments", func(t *testing.T) {
+		input := `
+penaltybox ip_pb {
+	// This is a comment
+}
+`
+		assertNoError(t, input)
+	})
+
+	t.Run("invalid penaltybox name", func(t *testing.T) {
+		input := `
+penaltybox vcl-recv {}
+	`
+		assertError(t, input)
+	})
+
+	t.Run("duplicate penaltybox declared", func(t *testing.T) {
+		input := `
+penaltybox ip_pb {}
+penaltybox ip_pb {}
+	`
+		assertError(t, input)
+	})
+
+	t.Run("penaltybox block is not empty", func(t *testing.T) {
+		input := `
+penaltybox ip_pb {
+	set var.bar = "baz";
+}
+`
+		assertError(t, input)
+	})
+}
+
+func TestLintRatecounterStatement(t *testing.T) {
+	t.Run("pass", func(t *testing.T) {
+		input := `
+ratecounter req_counter {}
+`
+		assertNoError(t, input)
+	})
+
+	t.Run("pass with comments", func(t *testing.T) {
+		input := `
+ratecounter req_counter {
+	// This is a comment
+}
+`
+		assertNoError(t, input)
+	})
+
+	t.Run("invalid ratecounter name", func(t *testing.T) {
+		input := `
+ratecounter vcl-recv {}
+	`
+		assertError(t, input)
+	})
+
+	t.Run("duplicate ratecounter declared", func(t *testing.T) {
+		input := `
+ratecounter req_counter {}
+ratecounter req_counter {}
+	`
+		assertError(t, input)
+	})
+
+	t.Run("ratecounter block is not empty", func(t *testing.T) {
+		input := `
+ratecounter req_counter {
+	set var.bar = "baz";
+}
+`
+		assertError(t, input)
+	})
+}
diff --git a/parser/declaration_parser_test.go b/parser/declaration_parser_test.go
index 6d4f6e7..90e243b 100644
--- a/parser/declaration_parser_test.go
+++ b/parser/declaration_parser_test.go
@@ -348,7 +348,7 @@ director example client {
 					&ast.DirectorBackendObject{
 						Meta: ast.New(T, 2, comments("// Leading comment"), comments("// Trailing comment")),
 						Values: []*ast.DirectorProperty{
-							&ast.DirectorProperty{
+							{
 								Meta: ast.New(T, 2),
 								Key: &ast.Ident{
 									Meta:  ast.New(T, 2),
@@ -359,7 +359,7 @@ director example client {
 									Value: "example",
 								},
 							},
-							&ast.DirectorProperty{
+							{
 								Meta: ast.New(T, 2),
 								Key: &ast.Ident{
 									Meta:  ast.New(T, 2),
@@ -382,3 +382,55 @@ director example client {
 	}
 	assert(t, vcl, expect)
 }
+
+func TestParsePenaltybox(t *testing.T) {
+	input := `// Penaltybox definition
+	penaltybox ip_pbox {
+} // Trailing comment`
+	expect := &ast.VCL{
+		Statements: []ast.Statement{
+			&ast.PenaltyboxDeclaration{
+				Meta: ast.New(T, 0, comments("// Penaltybox definition")),
+				Name: &ast.Ident{
+					Meta:  ast.New(T, 0),
+					Value: "ip_pbox",
+				},
+				Block: &ast.BlockStatement{
+					Meta:       ast.New(T, 1, ast.Comments{}, comments("// Trailing comment")),
+					Statements: []ast.Statement{},
+				},
+			},
+		},
+	}
+	vcl, err := New(lexer.NewFromString(input)).ParseVCL()
+	if err != nil {
+		t.Errorf("%+v\n", err)
+	}
+	assert(t, vcl, expect)
+}
+
+func TestParseRatecounter(t *testing.T) {
+	input := `// Ratecounter definition
+	ratecounter ip_ratecounter {
+} // Trailing comment`
+	expect := &ast.VCL{
+		Statements: []ast.Statement{
+			&ast.RatecounterDeclaration{
+				Meta: ast.New(T, 0, comments("// Ratecounter definition")),
+				Name: &ast.Ident{
+					Meta:  ast.New(T, 0),
+					Value: "ip_ratecounter",
+				},
+				Block: &ast.BlockStatement{
+					Meta:       ast.New(T, 1, ast.Comments{}, comments("// Trailing comment")),
+					Statements: []ast.Statement{},
+				},
+			},
+		},
+	}
+	vcl, err := New(lexer.NewFromString(input)).ParseVCL()
+	if err != nil {
+		t.Errorf("%+v\n", err)
+	}
+	assert(t, vcl, expect)
+}