Repository: ysugimoto/falco
Pull request #120 (2023-02-09 14:10:33)

Implement fastly managed snippets linting
Follows https://github.com/ysugimoto/falco/pull/119
Fixes: #118

Support Fastly managed VCL snippets -- regular and dynamic, also scope embed and include manually (none type).

1. Add remote logic that fetches regular and dynamic VCL snippets
2. Support `include "snippet::<snippet_name>` statement, root statement and block statement inside
3. Support macro extraction (e.g `FASTLY RECV`) and embed scope VCL snippets


Patch:
diff --git a/.golangci.yml b/.golangci.yml
index b129ce4..70fc44d 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -22,7 +22,7 @@ linters:
     - ineffassign
     - deadcode
     - dogsled
-    - dupl
+      #- dupl disable, because parser and lexer may have similer logic
     - funlen
     - gocognit
     - goconst
diff --git a/cmd/falco/runner.go b/cmd/falco/runner.go
index a61ecdb..3ffc0e5 100644
--- a/cmd/falco/runner.go
+++ b/cmd/falco/runner.go
@@ -57,6 +57,7 @@ type Fetcher interface {
 	Backends() ([]*types.RemoteBackend, error)
 	Dictionaries() ([]*types.RemoteDictionary, error)
 	Acls() ([]*types.RemoteAcl, error)
+	Snippets() ([]*types.RemoteVCL, error)
 }
 
 type RunMode int
diff --git a/cmd/falco/snippet.go b/cmd/falco/snippet.go
index 9ce993c..fe04160 100644
--- a/cmd/falco/snippet.go
+++ b/cmd/falco/snippet.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"fmt"
 	"regexp"
+	"sort"
 	"strings"
 
 	"text/template"
@@ -87,6 +88,13 @@ func (s *Snippet) Fetch() (*context.FastlySnippet, error) {
 	}
 	writeln(white, "Done")
 
+	write(white, "Fatching Snippets...")
+	fs.ScopedSnippets, fs.IncludeSnippets, err = s.fetchVCLSnippets()
+	if err != nil {
+		return nil, err
+	}
+	writeln(white, "Done")
+
 	return &fs, nil
 }
 
@@ -227,3 +235,43 @@ func (s *Snippet) renderBackendShields(backends []*types.RemoteBackend) ([]conte
 
 	return snippets, nil
 }
+
+func (s *Snippet) fetchVCLSnippets() (
+	map[string][]context.FastlySnippetItem,
+	map[string]context.FastlySnippetItem,
+	error,
+) {
+
+	snippets, err := s.fetcher.Snippets()
+	if err != nil {
+		return nil, nil, fmt.Errorf("Failed to get VCL snippets: %w", err)
+	}
+
+	// Sort by priority
+	sort.Slice(snippets, func(i, j int) bool {
+		return snippets[i].Priority > snippets[j].Priority
+	})
+
+	scoped := make(map[string][]context.FastlySnippetItem)
+	include := make(map[string]context.FastlySnippetItem)
+	for _, snip := range snippets {
+		// "none" type means that user could include the snippet arbitrary
+		if snip.Type == "none" {
+			include[snip.Name] = context.FastlySnippetItem{
+				Name: snip.Name,
+				Data: snip.Content,
+			}
+			continue
+		}
+		// Otherwise, factory with type (phase) name
+		if _, ok := scoped[snip.Type]; !ok {
+			scoped[snip.Type] = []context.FastlySnippetItem{}
+		}
+		scoped[snip.Type] = append(scoped[snip.Type], context.FastlySnippetItem{
+			Name: snip.Name,
+			Data: snip.Content,
+		})
+	}
+
+	return scoped, include, nil
+}
diff --git a/context/snippet.go b/context/snippet.go
index f14c2b4..ca9d5bc 100644
--- a/context/snippet.go
+++ b/context/snippet.go
@@ -6,9 +6,11 @@ type FastlySnippetItem struct {
 }
 
 type FastlySnippet struct {
-	Dictionaries []FastlySnippetItem
-	Acls         []FastlySnippetItem
-	Backends     []FastlySnippetItem
+	Dictionaries    []FastlySnippetItem
+	Acls            []FastlySnippetItem
+	Backends        []FastlySnippetItem
+	ScopedSnippets  map[string][]FastlySnippetItem
+	IncludeSnippets map[string]FastlySnippetItem
 }
 
 func (f *FastlySnippet) EmbedSnippets() []FastlySnippetItem {
diff --git a/docs/remote.md b/docs/remote.md
index 875f523..dfb9716 100644
--- a/docs/remote.md
+++ b/docs/remote.md
@@ -52,7 +52,41 @@ Currently not supported.
 
 ### VCL snippets
 
-Currently not supported.
+Prefetch [VCL Snippets](https://docs.fastly.com/en/guides/about-vcl-snippets) from Fastly and parse as `VCL`.
+falco support both of regular snippets and dynamic snippets, and could lint each scope snippets and `none` snippets that include manually.
+
+
+#### Root inclusion example
+
+You can use VCL Snippets in root of Custom VCL:
+
+```
+include "snippet::example_snippet";
+```
+
+#### Include in block statement
+
+You can include VCL Snippets in some of block statements (e.g sburoutine, if block, etc):
+
+```
+sub vcl_recv {
+  ...
+  include "snippet::example_snippet";
+  ...
+}
+```
+
+#### Fastly boilerplate macro extraction
+
+For example, if you create VCL Snippets and set type to `recv`, falco will extract it in place that boilerplate macro found:
+
+```
+
+sub vcl_recv {
+  #FASTLY RECV  <= falco find this and extract VCL Snippets here
+  ...
+}
+```
 
 ### Access Control Lists
 
diff --git a/linter/linter.go b/linter/linter.go
index 86a3f3a..66abe20 100644
--- a/linter/linter.go
+++ b/linter/linter.go
@@ -260,7 +260,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 {
 	// Resolve module, snippet inclusion
-	statements := l.resolveIncludeStatements(vcl.Statements, ctx)
+	statements := l.resolveIncludeStatements(vcl.Statements, ctx, true)
 
 	// https://github.com/ysugimoto/falco/issues/50
 	// To support subroutine hoisting, add root statements to context firstly and lint each statements after that.
@@ -274,7 +274,37 @@ func (l *Linter) lintVCL(vcl *ast.VCL, ctx *context.Context) types.Type {
 	return types.NeverType
 }
 
-func (l *Linter) resolveIncludeStatements(statements []ast.Statement, ctx *context.Context) []ast.Statement {
+func (l *Linter) loadSnippetVCL(file, content string) []ast.Statement {
+	lx := lexer.NewFromString(content, lexer.WithFile(file))
+	l.includexLexers[file] = lx
+	statements, err := parser.New(lx).ParseSnippetVCL()
+	if err != nil {
+		lx.NewLine()
+		l.FatalError = &FatalError{
+			Lexer: lx,
+			Error: errors.Cause(err),
+		}
+		return []ast.Statement{}
+	}
+	return statements
+}
+
+func (l *Linter) loadVCL(file, content string) []ast.Statement {
+	lx := lexer.NewFromString(content, lexer.WithFile(file))
+	l.includexLexers[file] = lx
+	vcl, err := parser.New(lx).ParseVCL()
+	if err != nil {
+		lx.NewLine()
+		l.FatalError = &FatalError{
+			Lexer: lx,
+			Error: errors.Cause(err),
+		}
+		return []ast.Statement{}
+	}
+	return vcl.Statements
+}
+
+func (l *Linter) resolveIncludeStatements(statements []ast.Statement, ctx *context.Context, isRoot bool) []ast.Statement {
 	var resolved []ast.Statement
 
 	for _, stmt := range statements {
@@ -286,36 +316,70 @@ func (l *Linter) resolveIncludeStatements(statements []ast.Statement, ctx *conte
 
 		// Check snippet inclusion
 		if strings.HasPrefix(include.Module.Value, "snippet::") {
-			// TODO: implement fastly managed snippet inclusion
+			resolved = append(resolved, l.resolveSnippetInclusion(include, ctx, isRoot)...)
 			continue
 		}
+		resolved = append(resolved, l.resolveFileInclusion(include, ctx, isRoot)...)
+	}
 
-		// Module (file) inclusion
-		module, err := ctx.Restore().Resolver().Resolve(include)
-		if err != nil {
-			e := &LintError{
-				Severity: ERROR,
-				Token:    include.GetMeta().Token,
-				Message:  err.Error(),
-			}
-			l.Error(e.Match(INCLUDE_STATEMENT_MODULE_LOAD_FAILED))
-			continue
+	return resolved
+}
+
+// Fastly managed snippet inclusion
+func (l *Linter) resolveSnippetInclusion(
+	include *ast.IncludeStatement,
+	ctx *context.Context,
+	isRoot bool, // if true, vcl would be included on root parsing
+) []ast.Statement {
+
+	var statements []ast.Statement
+	snippets := ctx.Snippets().IncludeSnippets
+	snip, ok := snippets[strings.TrimPrefix(include.Module.Value, "snippet::")]
+	if !ok {
+		e := &LintError{
+			Severity: ERROR,
+			Token:    include.GetMeta().Token,
+			Message: fmt.Sprintf(
+				"Snippet %s is not found on fastly managed snippets",
+				include.Module.Value,
+			),
 		}
-		lx := lexer.NewFromString(module.Data, lexer.WithFile(module.Name))
-		l.includexLexers[module.Name] = lx
-		vcl, err := parser.New(lx).ParseVCL()
-		if err != nil {
-			lx.NewLine()
-			l.FatalError = &FatalError{
-				Lexer: lx,
-				Error: errors.Cause(err),
-			}
-			continue
+		l.Error(e.Match(INCLUDE_STATEMENT_MODULE_NOT_FOUND))
+		return statements
+	}
+
+	// snippet could not have nested include statement
+	if isRoot {
+		return l.loadVCL(include.Module.Value, snip.Data)
+	}
+	return l.loadSnippetVCL(include.Module.Value, snip.Data)
+}
+
+// Module (file) inclusion
+func (l *Linter) resolveFileInclusion(
+	include *ast.IncludeStatement,
+	ctx *context.Context,
+	isRoot bool, // if true, vcl would be included on root parsing
+) []ast.Statement {
+
+	var statements []ast.Statement
+	module, err := ctx.Restore().Resolver().Resolve(include)
+	if err != nil {
+		e := &LintError{
+			Severity: ERROR,
+			Token:    include.GetMeta().Token,
+			Message:  err.Error(),
 		}
-		resolved = append(resolved, l.resolveIncludeStatements(vcl.Statements, ctx)...)
+		l.Error(e.Match(INCLUDE_STATEMENT_MODULE_LOAD_FAILED))
+		return statements
 	}
 
-	return resolved
+	if isRoot {
+		statements = l.loadVCL(module.Name, module.Data)
+	} else {
+		statements = l.loadSnippetVCL(module.Name, module.Data)
+	}
+	return l.resolveIncludeStatements(statements, ctx, isRoot)
 }
 
 //nolint:gocognit,funlen
@@ -749,17 +813,19 @@ func (l *Linter) lintSubRoutineDeclaration(decl *ast.SubroutineDeclaration, ctx
 		l.lintUnusedVariables(ctx)
 		cc.Restore()
 	}()
-	l.lint(decl.Block, cc)
-	// We are done linting inside the previous scope so
-	// we dont need the return type anymore
-	cc.ReturnType = nil
 
 	// If fastly reserved subroutine name (e.g vcl_recv, vcl_fetch, etc),
 	// validate fastly specific boilerplate macro is embedded like "FASTLY recv"
 	if scope := getFastlySubroutineScope(decl.Name.Value); scope != "" {
-		l.lintFastlyBoilerPlateMacro(decl, strings.ToUpper("FASTLY "+scope))
+		l.lintFastlyBoilerPlateMacro(decl, ctx, scope)
 	}
 
+	l.lint(decl.Block, cc)
+
+	// We are done linting inside the previous scope so
+	// we dont need the return type anymore
+	cc.ReturnType = nil
+
 	return types.NeverType
 }
 
@@ -823,15 +889,41 @@ func (l *Linter) lintRatecounterDeclaration(decl *ast.RatecounterDeclaration) ty
 	return types.NeverType
 }
 
-func (l *Linter) lintFastlyBoilerPlateMacro(sub *ast.SubroutineDeclaration, phrase string) {
+func (l *Linter) lintFastlyBoilerPlateMacro(sub *ast.SubroutineDeclaration, ctx *context.Context, scope string) {
+	phrase := strings.ToUpper("FASTLY " + scope)
+
+	// prepare scoped snippets
+	snippets, ok := ctx.Snippets().ScopedSnippets[scope]
+	if !ok {
+		snippets = []context.FastlySnippetItem{}
+	}
+
+	var resolved []ast.Statement
 	// visit all statement comments and find "FASTLY [phase]" comment
 	if hasFastlyBoilerPlateMacro(sub.Block.InfixComment(), phrase) {
+		for _, s := range snippets {
+			resolved = append(resolved, l.loadSnippetVCL("snippet::"+s.Name, s.Data)...)
+		}
+		sub.Block.Statements = append(resolved, sub.Block.Statements...)
 		return
 	}
+
+	var found bool
 	for _, stmt := range sub.Block.Statements {
-		if hasFastlyBoilerPlateMacro(stmt.LeadingComment(), phrase) {
-			return
+		if hasFastlyBoilerPlateMacro(stmt.LeadingComment(), phrase) && !found {
+			// Macro found but embedding snippets should do only once
+			for _, s := range snippets {
+				resolved = append(resolved, l.loadSnippetVCL("snippet::"+s.Name, s.Data)...)
+			}
+			found = true
 		}
+		resolved = append(resolved, stmt)
+	}
+	// assign resolved statements to subroutine block
+	sub.Block.Statements = resolved
+
+	if found {
+		return
 	}
 
 	// Macro not found
@@ -846,7 +938,7 @@ func (l *Linter) lintFastlyBoilerPlateMacro(sub *ast.SubroutineDeclaration, phra
 }
 
 func (l *Linter) lintBlockStatement(block *ast.BlockStatement, ctx *context.Context) types.Type {
-	statements := l.resolveIncludeStatements(block.Statements, ctx)
+	statements := l.resolveIncludeStatements(block.Statements, ctx, false)
 	for _, stmt := range statements {
 		l.lint(stmt, ctx)
 	}
diff --git a/parser/parser.go b/parser/parser.go
index 979ec6b..85b574a 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -209,3 +209,80 @@ func (p *Parser) parse() (ast.Statement, error) {
 	p.nextToken()
 	return stmt, nil
 }
+
+// ParseSnippetVCL is used for snippet parsing.
+// VCL snippet is a piece of vcl code so we should parse like BlockStatement inside,
+// and returns slice of statement.
+func (p *Parser) ParseSnippetVCL() ([]ast.Statement, error) {
+	var statements []ast.Statement
+
+	for !p.peekTokenIs(token.EOF) {
+		var stmt ast.Statement
+		var err error
+
+		switch p.curToken.Token.Type {
+		// https://github.com/ysugimoto/falco/issues/17
+		// VCL accepts block syntax:
+		// ```
+		// sub vcl_recv {
+		//   {
+		//      log "recv";
+		//   }
+		// }
+		// ```
+		case token.LEFT_BRACE:
+			stmt, err = p.parseBlockStatement()
+		case token.SET:
+			stmt, err = p.parseSetStatement()
+		case token.UNSET:
+			stmt, err = p.parseUnsetStatement()
+		case token.REMOVE:
+			stmt, err = p.parseRemoveStatement()
+		case token.ADD:
+			stmt, err = p.parseAddStatement()
+		case token.CALL:
+			stmt, err = p.parseCallStatement()
+		case token.DECLARE:
+			stmt, err = p.parseDeclareStatement()
+		case token.ERROR:
+			stmt, err = p.parseErrorStatement()
+		case token.ESI:
+			stmt, err = p.parseEsiStatement()
+		case token.LOG:
+			stmt, err = p.parseLogStatement()
+		case token.RESTART:
+			stmt, err = p.parseRestartStatement()
+		case token.RETURN:
+			stmt, err = p.parseReturnStatement()
+		case token.SYNTHETIC:
+			stmt, err = p.parseSyntheticStatement()
+		case token.SYNTHETIC_BASE64:
+			stmt, err = p.parseSyntheticBase64Statement()
+		case token.IF:
+			stmt, err = p.parseIfStatement()
+		case token.GOTO:
+			stmt, err = p.parseGotoStatement()
+		case token.INCLUDE:
+			stmt, err = p.parseIncludeStatement()
+		case token.IDENT:
+			// Check if the current ident is a function call
+			if p.peekTokenIs(token.LEFT_PAREN) {
+				stmt, err = p.parseFunctionCall()
+			} else {
+				// Could be a goto destination
+				stmt, err = p.parseGotoDestination()
+			}
+		default:
+			err = UnexpectedToken(p.peekToken)
+		}
+
+		if err != nil {
+			return nil, errors.WithStack(err)
+		}
+		statements = append(statements, stmt)
+		p.nextToken() // point to statement
+	}
+
+	p.nextToken() // point to EOF
+	return statements, nil
+}
diff --git a/parser/statement_parser.go b/parser/statement_parser.go
index 26f0d14..5133cd4 100644
--- a/parser/statement_parser.go
+++ b/parser/statement_parser.go
@@ -45,6 +45,7 @@ func (p *Parser) parseIncludeStatement() (ast.Statement, error) {
 	return i, nil
 }
 
+// nolint: dupl
 func (p *Parser) parseBlockStatement() (*ast.BlockStatement, error) {
 	// Note: block statement is used for declaration/statement inside like subroutine, if, elseif, else
 	// on start this statement, current token must point start of LEFT_BRACE
diff --git a/remote/client.go b/remote/client.go
index c9db546..41f76d1 100644
--- a/remote/client.go
+++ b/remote/client.go
@@ -168,3 +168,56 @@ func (c *FastlyClient) ListBackends(ctx context.Context, version int64) ([]*Back
 
 	return backends, nil
 }
+
+func (c *FastlyClient) ListSnippets(ctx context.Context, version int64) ([]*VCLSnippet, error) {
+	endpoint := fmt.Sprintf("/service/%s/version/%d/snippet", c.serviceId, version)
+	var snippets []*VCLSnippet
+	if err := c.request(ctx, endpoint, &snippets); err != nil {
+		return nil, errors.WithStack(err)
+	}
+
+	// Dynamic snippet contents is nil for this API, we need to call more API to get snippet content
+	var wg sync.WaitGroup
+	var once sync.Once
+	errch := make(chan error)
+	for _, s := range snippets {
+		wg.Add(1)
+		go func(s *VCLSnippet) {
+			defer wg.Done()
+			if s.Dynamic == "0" {
+				return
+			}
+			var err error
+			if s.Content, err = c.getDynamicSnippetContent(ctx, s.Id); err != nil {
+				once.Do(func() {
+					errch <- err
+				})
+			}
+		}(s)
+	}
+
+	done := make(chan struct{})
+	go func() {
+		wg.Wait()
+		close(done)
+	}()
+
+	select {
+	case <-done:
+		return snippets, nil
+	case err := <-errch:
+		return nil, err
+	}
+}
+
+func (c *FastlyClient) getDynamicSnippetContent(ctx context.Context, snippetId string) (*string, error) {
+	endpoint := fmt.Sprintf("/service/%s/snippet/%s", c.serviceId, snippetId)
+	var snippet struct {
+		Content string `json:"content"`
+	}
+	if err := c.request(ctx, endpoint, &snippet); err != nil {
+		return nil, errors.WithStack(err)
+	}
+
+	return &snippet.Content, nil
+}
diff --git a/remote/entity.go b/remote/entity.go
index 782ed06..309364d 100644
--- a/remote/entity.go
+++ b/remote/entity.go
@@ -46,3 +46,12 @@ type Director struct {
 	Type     DirectorType `json:"type"`
 	Backends []string     `json:"backends"`
 }
+
+type VCLSnippet struct {
+	Id       string  `json:"id"`
+	Name     string  `json:"name"`
+	Dynamic  string  `json:"dynamic"`
+	Type     string  `json:"type"`
+	Priority string  `json:"priority"`
+	Content  *string `json:"content"`
+}
diff --git a/remote/fetcher.go b/remote/fetcher.go
index 9a54421..3a3b9cd 100644
--- a/remote/fetcher.go
+++ b/remote/fetcher.go
@@ -1,9 +1,11 @@
 package remote
 
 import (
+	"context"
 	_context "context"
 	"fmt"
 	"net/http"
+	"strconv"
 	"sync"
 	"time"
 
@@ -103,3 +105,34 @@ func (f *FastlyApiFetcher) getVersion(c _context.Context) (int64, error) {
 	f.version = v
 	return v, nil
 }
+
+func (f *FastlyApiFetcher) Snippets() ([]*types.RemoteVCL, error) {
+	c, timeout := context.WithTimeout(_context.Background(), f.timeout)
+	defer timeout()
+
+	version, err := f.getVersion(c)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to get latest version %w", err)
+	}
+
+	fastlyVcls, err := f.client.ListSnippets(c, version)
+	if err != nil {
+		return nil, err
+	}
+
+	var r []*types.RemoteVCL
+	for _, v := range fastlyVcls {
+		p, err := strconv.ParseInt(v.Priority, 10, 64)
+		if err != nil {
+			return nil, fmt.Errorf("Failed to convert VCL snippet priority to int: %w", err)
+		}
+
+		r = append(r, &types.RemoteVCL{
+			Name:     v.Name,
+			Type:     v.Type,
+			Content:  *v.Content,
+			Priority: p,
+		})
+	}
+	return r, nil
+}
diff --git a/terraform/fetcher.go b/terraform/fetcher.go
index 6f1b350..3c23aea 100644
--- a/terraform/fetcher.go
+++ b/terraform/fetcher.go
@@ -50,3 +50,18 @@ func (f *TerraformFetcher) Acls() ([]*types.RemoteAcl, error) {
 	}
 	return a, nil
 }
+
+func (f *TerraformFetcher) Snippets() ([]*types.RemoteVCL, error) {
+	var v []*types.RemoteVCL
+	for _, s := range f.services {
+		for _, vcl := range s.Snippets {
+			v = append(v, &types.RemoteVCL{
+				Name:     vcl.Name,
+				Type:     vcl.Type,
+				Content:  vcl.Content,
+				Priority: vcl.Priority,
+			})
+		}
+	}
+	return v, nil
+}
diff --git a/terraform/terraform.go b/terraform/terraform.go
index c89e218..08fcd29 100644
--- a/terraform/terraform.go
+++ b/terraform/terraform.go
@@ -28,6 +28,13 @@ type TerraformDictionary struct {
 	Name string
 }
 
+type TerraformSnippet struct {
+	Name     string
+	Type     string
+	Content  string
+	Priority int64
+}
+
 // TODO(davinci26): We can unmarshall all the properties from the TF file
 // and lint them to make sure they have sane values.
 type TerraformBackend struct {
@@ -41,6 +48,7 @@ type FastlyService struct {
 	Backends     []*TerraformBackend
 	Acls         []*TerraformAcl
 	Dictionaries []*TerraformDictionary
+	Snippets     []*TerraformSnippet
 }
 
 type TerraformPlannedResource struct {
@@ -52,6 +60,7 @@ type TerraformPlannedResource struct {
 		Acl        []*TerraformAcl        `json:"acl"`
 		Backend    []*TerraformBackend    `json:"backend"`
 		Dictionary []*TerraformDictionary `json:"dictionary"`
+		Snippets   []*TerraformSnippet    `json:"snippet"`
 	} `json:"values"`
 }
 
@@ -95,6 +104,7 @@ func UnmarshalTerraformPlannedInput(buf []byte) ([]*FastlyService, error) {
 				Acls:         v.Values.Acl,
 				Backends:     v.Values.Backend,
 				Dictionaries: v.Values.Dictionary,
+				Snippets:     v.Values.Snippets,
 			})
 		}
 	}
@@ -112,6 +122,7 @@ func UnmarshalTerraformPlannedInput(buf []byte) ([]*FastlyService, error) {
 				Acls:         v.Values.Acl,
 				Backends:     v.Values.Backend,
 				Dictionaries: v.Values.Dictionary,
+				Snippets:     v.Values.Snippets,
 			})
 		}
 	}
diff --git a/token/token.go b/token/token.go
index 2cf4d39..562c5cb 100644
--- a/token/token.go
+++ b/token/token.go
@@ -13,6 +13,7 @@ type Token struct {
 	Position int
 	Offset   int    // for print problem
 	File     string // for print problem
+	Snippet  bool
 }
 
 func (t Token) String() string {
diff --git a/types/types.go b/types/types.go
index c53623d..e44ec1b 100644
--- a/types/types.go
+++ b/types/types.go
@@ -297,3 +297,10 @@ type RemoteBackend struct {
 	Name   string
 	Shield *string
 }
+
+type RemoteVCL struct {
+	Name     string
+	Type     string
+	Content  string
+	Priority int64
+}


Test patch:
diff --git a/linter/linter_test.go b/linter/linter_test.go
index 63a1443..18d1873 100644
--- a/linter/linter_test.go
+++ b/linter/linter_test.go
@@ -1,10 +1,10 @@
 package linter
 
 import (
+	"errors"
 	"fmt"
 	"testing"
 
-	"github.com/pkg/errors"
 	"github.com/ysugimoto/falco/ast"
 	"github.com/ysugimoto/falco/context"
 	"github.com/ysugimoto/falco/lexer"
@@ -25,6 +25,9 @@ func assertNoError(t *testing.T, input string, opts ...context.Option) {
 	if len(l.Errors) > 0 {
 		t.Errorf("Lint error: %s", l.Errors)
 	}
+	if l.FatalError != nil {
+		t.Errorf("Fatal error: %s", l.FatalError.Error)
+	}
 }
 
 func assertError(t *testing.T, input string, opts ...context.Option) {
@@ -39,6 +42,9 @@ func assertError(t *testing.T, input string, opts ...context.Option) {
 	if len(l.Errors) == 0 {
 		t.Errorf("Expect one lint error but empty returned")
 	}
+	if l.FatalError != nil {
+		t.Errorf("Fatal error: %s", l.FatalError.Error)
+	}
 }
 func assertErrorWithSeverity(t *testing.T, input string, severity Severity, opts ...context.Option) {
 	vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
@@ -2488,3 +2494,44 @@ sub vcl_recv {
 		`
 	assertNoError(t, input, context.WithResolver(mock))
 }
+
+func TestFastlyScopedSnippetInclusion(t *testing.T) {
+	snippets := &context.FastlySnippet{
+		ScopedSnippets: map[string][]context.FastlySnippetItem{
+			"recv": {
+				{
+					Name: "recv_injection",
+					Data: `set req.http.InjectedViaMacro = 1;`,
+				},
+			},
+		},
+	}
+	input := `
+sub vcl_recv {
+   #FASTLY RECV
+
+   return (pass);
+}
+`
+	assertError(t, input, context.WithFastlySnippets(snippets))
+}
+
+func TestFastlySnippetInclusion(t *testing.T) {
+	snippets := &context.FastlySnippet{
+		IncludeSnippets: map[string]context.FastlySnippetItem{
+			"recv_injection": {
+				Name: "recv_injection",
+				Data: `set req.http.InjectedViaMacro = 1;`,
+			},
+		},
+	}
+	input := `
+sub vcl_recv {
+   #FASTLY RECV
+   if (req.http.Some-Truthy) {
+	  include "snippet::recv_injection";
+   }
+}
+`
+	assertError(t, input, context.WithFastlySnippets(snippets))
+}