Repository: ysugimoto/falco
Pull request #119 (2023-02-09 04:44:28)

Lint include statement on its place
This PR improves `include` statement resolving that it should be solved in Linter context.
By resolving in Linter context, we could do linting a DFS-like style.

And also, this is preparation for solving the issue of https://github.com/ysugimoto/falco/issues/118, to solve in Linter context, we could support including Fastly managed snippets as well 👍 



Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 0ebbe48..b129ce4 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -7,6 +7,7 @@ run:
     - dist
     - docs
     - .github
+    - playground
 
 linters:
   disable-all: true
diff --git a/__generator__/builtin.yml b/__generator__/builtin.yml
index 6ef0eb2..094c224 100644
--- a/__generator__/builtin.yml
+++ b/__generator__/builtin.yml
@@ -16,7 +16,7 @@ accept.language_filter_basic:
   reference: "https://developer.fastly.com/reference/vcl/functions/content-negotiation/accept-language-filter-basic/"
   on: [RECV, HASH, HIT, MISS, PASS, FETCH, ERROR, DELIVER, LOG]
   arguments:
-    - [STRING, STRING, STRING, STRING]
+    - [STRING, STRING, STRING, INTEGER]
   return: STRING
 
 accept.language_lookup:
diff --git a/cmd/falco/main.go b/cmd/falco/main.go
index 5055f00..858c6b9 100644
--- a/cmd/falco/main.go
+++ b/cmd/falco/main.go
@@ -12,6 +12,7 @@ import (
 	"github.com/kyokomi/emoji"
 	"github.com/mattn/go-colorable"
 	"github.com/pkg/errors"
+	"github.com/ysugimoto/falco/resolver"
 	"github.com/ysugimoto/falco/terraform"
 )
 
@@ -139,20 +140,20 @@ func main() {
 
 	var fetcher Fetcher
 	// falco could lint multiple services so resolver should be a slice
-	var resolvers []Resolver
+	var resolvers []resolver.Resolver
 	switch fs.Arg(0) {
 	case subcommandTerraform:
 		fastlyServices, err := ParseStdin()
 		if err == nil {
-			resolvers = NewTerraformResolver(fastlyServices)
+			resolvers = resolver.NewTerraformResolver(fastlyServices)
 			fetcher = terraform.NewTerraformFetcher(fastlyServices)
 		}
 	case subcommandLint:
 		// "lint" command provides single file of service, then resolvers size is always 1
-		resolvers, err = NewFileResolvers(fs.Arg(1), c)
+		resolvers, err = resolver.NewFileResolvers(fs.Arg(1), c.IncludePaths)
 	default:
 		// "lint" command provides single file of service, then resolvers size is always 1
-		resolvers, err = NewFileResolvers(fs.Arg(0), c)
+		resolvers, err = resolver.NewFileResolvers(fs.Arg(0), c.IncludePaths)
 	}
 
 	if err != nil {
@@ -167,7 +168,7 @@ func main() {
 			writeln(white, strings.Repeat("=", 18+len(name)))
 		}
 
-		runner, err := NewRunner(v, c, fetcher)
+		runner, err := NewRunner(c, fetcher)
 		if err != nil {
 			writeln(red, err.Error())
 			os.Exit(1)
@@ -175,9 +176,9 @@ func main() {
 
 		var exitErr error
 		if c.Stats {
-			exitErr = runStats(runner, c.Json)
+			exitErr = runStats(runner, v, c.Json)
 		} else {
-			exitErr = runLint(runner)
+			exitErr = runLint(runner, v)
 		}
 		if exitErr == ErrExit {
 			shouldExit = true
@@ -189,8 +190,8 @@ func main() {
 	}
 }
 
-func runLint(runner *Runner) error {
-	result, err := runner.Run()
+func runLint(runner *Runner, rslv resolver.Resolver) error {
+	result, err := runner.Run(rslv)
 	if err != nil {
 		if err != ErrParser {
 			writeln(red, err.Error())
@@ -235,8 +236,8 @@ func runLint(runner *Runner) error {
 	return nil
 }
 
-func runStats(runner *Runner, printJson bool) error {
-	stats, err := runner.Stats()
+func runStats(runner *Runner, rslv resolver.Resolver, printJson bool) error {
+	stats, err := runner.Stats(rslv)
 	if err != nil {
 		if err != ErrParser {
 			writeln(red, err.Error())
diff --git a/cmd/falco/runner.go b/cmd/falco/runner.go
index fb87bf5..a61ecdb 100644
--- a/cmd/falco/runner.go
+++ b/cmd/falco/runner.go
@@ -18,6 +18,7 @@ import (
 	"github.com/ysugimoto/falco/parser"
 	"github.com/ysugimoto/falco/plugin"
 	"github.com/ysugimoto/falco/remote"
+	"github.com/ysugimoto/falco/resolver"
 	"github.com/ysugimoto/falco/types"
 )
 
@@ -67,26 +68,20 @@ const (
 
 type Runner struct {
 	transformers []*Transformer
-	resolver     Resolver
 	overrides    map[string]linter.Severity
 	lexers       map[string]*lexer.Lexer
-	snippets     []snippetItem
+	snippets     *context.FastlySnippet
 
 	level Level
 
-	// Note: this context is not Go context, our parsing context :)
-	context *context.Context
-
 	// runner result fields
 	infos    int
 	warnings int
 	errors   int
 }
 
-func NewRunner(rz Resolver, c *Config, f Fetcher) (*Runner, error) {
+func NewRunner(c *Config, f Fetcher) (*Runner, error) {
 	r := &Runner{
-		resolver:  rz,
-		context:   context.New(),
 		level:     LevelError,
 		overrides: make(map[string]linter.Severity),
 		lexers:    make(map[string]*lexer.Lexer),
@@ -123,7 +118,7 @@ func NewRunner(rz Resolver, c *Config, f Fetcher) (*Runner, error) {
 		if err != nil {
 			writeln(red, err.Error())
 		}
-		r.snippets = append(r.snippets, snippets...)
+		r.snippets = snippets
 	}
 
 	// Check transformer exists and format to absolute path
@@ -214,30 +209,21 @@ func (r *Runner) Transform(vcl *plugin.VCL) error {
 	return nil
 }
 
-func (r *Runner) parseVCL(name, code string) (*ast.VCL, error) {
-	lx := lexer.NewFromString(code, lexer.WithFile(name))
-	p := parser.New(lx)
-	vcl, err := p.ParseVCL()
-	if err != nil {
-		lx.NewLine()
-		pe, ok := errors.Cause(err).(*parser.ParseError)
-		if ok {
-			r.printParseError(lx, pe)
-		}
-		return nil, ErrParser
+func (r *Runner) Run(rslv resolver.Resolver) (*RunnerResult, error) {
+	options := []context.Option{context.WithResolver(rslv)}
+	// If remote snippets exists, prepare parse and prepend to main VCL
+	if r.snippets != nil {
+		options = append(options, context.WithFastlySnippets(r.snippets))
 	}
-	lx.NewLine()
-	r.lexers[name] = lx
-	return vcl, nil
-}
 
-func (r *Runner) Run() (*RunnerResult, error) {
-	main, err := r.resolver.MainVCL()
+	main, err := rslv.MainVCL()
 	if err != nil {
 		return nil, err
 	}
 
-	vcl, err := r.run(main, RunModeLint)
+	// Note: this context is not Go context, our parsing context :)
+	ctx := context.New(options...)
+	vcl, err := r.run(ctx, main, RunModeLint)
 	if err != nil {
 		return nil, err
 	}
@@ -250,114 +236,74 @@ func (r *Runner) Run() (*RunnerResult, error) {
 	}, nil
 }
 
-func (r *Runner) run(v *VCL, mode RunMode) (*plugin.VCL, error) {
-	vcl, err := r.parseVCL(v.Name, v.Data)
+func (r *Runner) run(ctx *context.Context, main *resolver.VCL, mode RunMode) (*plugin.VCL, error) {
+	vcl, err := r.parseVCL(main.Name, main.Data)
 	if err != nil {
 		return nil, err
 	}
 
 	// If remote snippets exists, prepare parse and prepend to main VCL
-	for _, snip := range r.snippets {
-		s, err := r.parseVCL(snip.Name, snip.Data)
-		if err != nil {
-			return nil, err
+	if r.snippets != nil {
+		for _, snip := range r.snippets.EmbedSnippets() {
+			s, err := r.parseVCL(snip.Name, snip.Data)
+			if err != nil {
+				return nil, err
+			}
+			vcl.Statements = append(s.Statements, vcl.Statements...)
 		}
-		vcl.Statements = append(s.Statements, vcl.Statements...)
-	}
-
-	vcl.Statements, err = r.resolveStatements(vcl.Statements)
-	if err != nil {
-		return nil, err
 	}
 
 	lt := linter.New()
-	lt.Lint(vcl, r.context)
+	lt.Lint(vcl, ctx)
+
+	for k, v := range lt.Lexers() {
+		r.lexers[k] = v
+	}
 
 	// If runner is running as stat mode, prevent to output lint result
 	if mode&RunModeStat > 0 {
 		return nil, nil
 	}
 
+	// Checking Fatal error, it means parse error occurs on included submodule
+	if lt.FatalError != nil {
+		if pe, ok := lt.FatalError.Error.(*parser.ParseError); ok {
+			r.printParseError(lt.FatalError.Lexer, pe)
+		}
+		return nil, ErrParser
+	}
+
 	if len(lt.Errors) > 0 {
 		for _, err := range lt.Errors {
 			le, ok := err.(*linter.LintError)
 			if !ok {
 				continue
 			}
-			r.printLinterError(r.lexers[v.Name], le)
+			r.printLinterError(r.lexers[main.Name], le)
 		}
 	}
 
 	return &plugin.VCL{
-		File: v.Name,
+		File: main.Name,
 		AST:  vcl,
 	}, nil
 }
 
-func (r *Runner) resolveStatements(statements []ast.Statement) ([]ast.Statement, error) {
-	var resolved []ast.Statement
-
-	for _, stmt := range statements {
-		switch t := stmt.(type) {
-		case *ast.IncludeStatement:
-			module, err := r.resolver.Resolve(t.Module.Value)
-			if err != nil {
-				return nil, err
-			}
-
-			vcl, err := r.parseVCL(module.Name, module.Data)
-			if err != nil {
-				return nil, err
-			}
-
-			vcl.Statements, err = r.resolveStatements(vcl.Statements)
-			if err != nil {
-				return nil, err
-			}
-			resolved = append(resolved, vcl.Statements...)
-		case *ast.IfStatement:
-			if err := r.resolveIfStatement(t); err != nil {
-				return nil, err
-			}
-			resolved = append(resolved, t)
-		case *ast.SubroutineDeclaration:
-			ss, err := r.resolveStatements(t.Block.Statements)
-			if err != nil {
-				return nil, err
-			}
-			t.Block.Statements = ss
-			resolved = append(resolved, t)
-		default:
-			resolved = append(resolved, t)
-		}
-	}
-	return resolved, nil
-}
-
-func (r *Runner) resolveIfStatement(s *ast.IfStatement) error {
-	if s.Consequence != nil {
-		ss, err := r.resolveStatements(s.Consequence.Statements)
-		if err != nil {
-			return err
-		}
-		s.Consequence.Statements = ss
-	}
-
-	for _, a := range s.Another {
-		if err := r.resolveIfStatement(a); err != nil {
-			return err
-		}
-	}
-
-	if s.Alternative != nil {
-		ss, err := r.resolveStatements(s.Alternative.Statements)
-		if err != nil {
-			return err
+func (r *Runner) parseVCL(name, code string) (*ast.VCL, error) {
+	lx := lexer.NewFromString(code, lexer.WithFile(name))
+	p := parser.New(lx)
+	vcl, err := p.ParseVCL()
+	if err != nil {
+		lx.NewLine()
+		pe, ok := errors.Cause(err).(*parser.ParseError)
+		if ok {
+			r.printParseError(lx, pe)
 		}
-		s.Alternative.Statements = ss
+		return nil, ErrParser
 	}
-
-	return nil
+	lx.NewLine()
+	r.lexers[name] = lx
+	return vcl, nil
 }
 
 func (r *Runner) printParseError(lx *lexer.Lexer, err *parser.ParseError) {
@@ -399,6 +345,7 @@ func (r *Runner) printLinterError(lx *lexer.Lexer, err *linter.LintError) {
 	if err.Token.File != "" {
 		file = "in " + err.Token.File + " "
 		lx = r.lexers[err.Token.File]
+		fmt.Println(err.Token.File, lx)
 	}
 
 	// check severity with overrides
@@ -455,23 +402,32 @@ func (r *Runner) printLinterError(lx *lexer.Lexer, err *linter.LintError) {
 	writeln(white, "")
 }
 
-func (r *Runner) Stats() (*StatsResult, error) {
-	main, err := r.resolver.MainVCL()
+func (r *Runner) Stats(rslv resolver.Resolver) (*StatsResult, error) {
+	options := []context.Option{context.WithResolver(rslv)}
+	// If remote snippets exists, prepare parse and prepend to main VCL
+	if r.snippets != nil {
+		options = append(options, context.WithFastlySnippets(r.snippets))
+	}
+
+	main, err := rslv.MainVCL()
 	if err != nil {
 		return nil, err
 	}
 
-	if _, err := r.run(main, RunModeStat); err != nil {
+	// Note: this context is not Go context, our parsing context :)
+	ctx := context.New(options...)
+
+	if _, err := r.run(ctx, main, RunModeStat); err != nil {
 		return nil, err
 	}
 
 	stats := &StatsResult{
 		Main:        main.Name,
-		Subroutines: len(r.context.Subroutines),
-		Tables:      len(r.context.Tables),
-		Backends:    len(r.context.Backends),
-		Acls:        len(r.context.Acls),
-		Directors:   len(r.context.Directors),
+		Subroutines: len(ctx.Subroutines),
+		Tables:      len(ctx.Tables),
+		Backends:    len(ctx.Backends),
+		Acls:        len(ctx.Acls),
+		Directors:   len(ctx.Directors),
 	}
 
 	for _, lx := range r.lexers {
diff --git a/cmd/falco/snippet.go b/cmd/falco/snippet.go
index 07aabb3..9ce993c 100644
--- a/cmd/falco/snippet.go
+++ b/cmd/falco/snippet.go
@@ -8,6 +8,7 @@ import (
 
 	"text/template"
 
+	"github.com/ysugimoto/falco/context"
 	"github.com/ysugimoto/falco/remote"
 	"github.com/ysugimoto/falco/types"
 )
@@ -51,11 +52,6 @@ func TerraformBackendNameSanitizer(name string) string {
 	return s
 }
 
-type snippetItem struct {
-	Data string
-	Name string
-}
-
 type Snippet struct {
 	fetcher Fetcher
 }
@@ -66,38 +62,36 @@ func NewSnippet(f Fetcher) *Snippet {
 	}
 }
 
-func (s *Snippet) Fetch() ([]snippetItem, error) {
-	var snippets []snippetItem
+func (s *Snippet) Fetch() (*context.FastlySnippet, error) {
+	var fs context.FastlySnippet
+	var err error
 
 	write(white, "Fetching Edge Dictionaries...")
-	dicts, err := s.fetchEdgeDictionary()
+	fs.Dictionaries, err = s.fetchEdgeDictionary()
 	if err != nil {
 		return nil, err
 	}
 	writeln(white, "Done")
-	snippets = append(snippets, dicts...)
 
 	write(white, "Fatching Access Control Lists...")
-	acls, err := s.fetchAccessControl()
+	fs.Acls, err = s.fetchAccessControl()
 	if err != nil {
 		return nil, err
 	}
 	writeln(white, "Done")
-	snippets = append(snippets, acls...)
 
 	write(white, "Fatching Backends...")
-	backends, err := s.fetchBackend()
+	fs.Backends, err = s.fetchBackend()
 	if err != nil {
 		return nil, err
 	}
 	writeln(white, "Done")
-	snippets = append(snippets, backends...)
 
-	return snippets, nil
+	return &fs, nil
 }
 
 // Fetch remote Edge dictionary items
-func (s *Snippet) fetchEdgeDictionary() ([]snippetItem, error) {
+func (s *Snippet) fetchEdgeDictionary() ([]context.FastlySnippetItem, error) {
 	dicts, err := s.fetcher.Dictionaries()
 	if err != nil {
 		return nil, fmt.Errorf("Failed to get edge dictionaries %w", err)
@@ -108,13 +102,13 @@ func (s *Snippet) fetchEdgeDictionary() ([]snippetItem, error) {
 		return nil, fmt.Errorf("Failed to compilte table template: %w", err)
 	}
 
-	var snippets []snippetItem
+	var snippets []context.FastlySnippetItem
 	for _, dict := range dicts {
 		buf := new(bytes.Buffer)
 		if err := tmpl.Execute(buf, dict); err != nil {
 			return nil, fmt.Errorf("Failed to render table template: %w", err)
 		}
-		snippets = append(snippets, snippetItem{
+		snippets = append(snippets, context.FastlySnippetItem{
 			Name: fmt.Sprintf("EdgeDictionary:%s", dict.Name),
 			Data: buf.String(),
 		})
@@ -123,7 +117,7 @@ func (s *Snippet) fetchEdgeDictionary() ([]snippetItem, error) {
 }
 
 // Fetch remote Access Control entries
-func (s *Snippet) fetchAccessControl() ([]snippetItem, error) {
+func (s *Snippet) fetchAccessControl() ([]context.FastlySnippetItem, error) {
 	acls, err := s.fetcher.Acls()
 	if err != nil {
 		return nil, fmt.Errorf("Failed to get ACLs: %w", err)
@@ -134,13 +128,13 @@ func (s *Snippet) fetchAccessControl() ([]snippetItem, error) {
 		return nil, fmt.Errorf("Failed to compile acl template: %w", err)
 	}
 
-	var snippets []snippetItem
+	var snippets []context.FastlySnippetItem
 	for _, a := range acls {
 		buf := new(bytes.Buffer)
 		if err := tmpl.Execute(buf, a); err != nil {
 			return nil, fmt.Errorf("Failed to render acl template: %w", err)
 		}
-		snippets = append(snippets, snippetItem{
+		snippets = append(snippets, context.FastlySnippetItem{
 			Name: fmt.Sprintf("ACL:%s", a.Name),
 			Data: buf.String(),
 		})
@@ -148,8 +142,8 @@ func (s *Snippet) fetchAccessControl() ([]snippetItem, error) {
 	return snippets, nil
 }
 
-func (s *Snippet) fetchBackend() ([]snippetItem, error) {
-	var snippets []snippetItem
+func (s *Snippet) fetchBackend() ([]context.FastlySnippetItem, error) {
+	var snippets []context.FastlySnippetItem
 	backends, err := s.fetcher.Backends()
 	if err != nil {
 		return nil, fmt.Errorf("Failed to get Backends: %w", err)
@@ -168,7 +162,7 @@ func (s *Snippet) fetchBackend() ([]snippetItem, error) {
 		if err := backTmpl.Execute(buf, b); err != nil {
 			return nil, fmt.Errorf("failed to render backend template: %w", err)
 		}
-		snippets = append(snippets, snippetItem{
+		snippets = append(snippets, context.FastlySnippetItem{
 			Name: fmt.Sprintf("BACKEND:%s", b.Name),
 			Data: buf.String(),
 		})
@@ -186,7 +180,7 @@ func (s *Snippet) fetchBackend() ([]snippetItem, error) {
 	return snippets, nil
 }
 
-func (s *Snippet) renderBackendShields(backends []*types.RemoteBackend) ([]snippetItem, error) {
+func (s *Snippet) renderBackendShields(backends []*types.RemoteBackend) ([]context.FastlySnippetItem, error) {
 	printType := func(dtype remote.DirectorType) string {
 		switch dtype {
 		case remote.Random:
@@ -212,7 +206,7 @@ func (s *Snippet) renderBackendShields(backends []*types.RemoteBackend) ([]snipp
 		}
 	}
 
-	var snippets []snippetItem
+	var snippets []context.FastlySnippetItem
 	// We need to pick an arbitrary backend to avoid an undeclared linter error
 	shieldBackend := "F_" + backends[0].Name
 	for sd := range shieldDirectors {
@@ -225,7 +219,7 @@ func (s *Snippet) renderBackendShields(backends []*types.RemoteBackend) ([]snipp
 		if err := dirTmpl.Execute(buf, d); err != nil {
 			return nil, fmt.Errorf("failed to render director template: %w", err)
 		}
-		snippets = append(snippets, snippetItem{
+		snippets = append(snippets, context.FastlySnippetItem{
 			Name: fmt.Sprintf("DIRECTOR:%s", d.Name),
 			Data: buf.String(),
 		})
diff --git a/context/builtin.go b/context/builtin.go
index 0d33407..de88cc7 100644
--- a/context/builtin.go
+++ b/context/builtin.go
@@ -53,7 +53,7 @@ func builtinFunctions() Functions {
 					Value: &BuiltinFunction{
 						Return: types.StringType,
 						Arguments: [][]types.Type{
-							[]types.Type{types.StringType, types.StringType, types.StringType, types.StringType},
+							[]types.Type{types.StringType, types.StringType, types.StringType, types.IntegerType},
 						},
 						Scopes:    RECV | HASH | HIT | MISS | PASS | FETCH | ERROR | DELIVER | LOG,
 						Reference: "https://developer.fastly.com/reference/vcl/functions/content-negotiation/accept-language-filter-basic/",
diff --git a/context/context.go b/context/context.go
index ed2f723..72dca1e 100644
--- a/context/context.go
+++ b/context/context.go
@@ -5,6 +5,7 @@ import (
 	"strings"
 
 	"github.com/ysugimoto/falco/ast"
+	"github.com/ysugimoto/falco/resolver"
 	"github.com/ysugimoto/falco/types"
 )
 
@@ -124,10 +125,12 @@ type Context struct {
 	Variables      Variables
 	RegexVariables map[string]int
 	ReturnType     *types.Type
+	resolver       resolver.Resolver
+	fastlySnippets *FastlySnippet
 }
 
-func New() *Context {
-	return &Context{
+func New(opts ...Option) *Context {
+	c := &Context{
 		curMode:        RECV,
 		curName:        "vcl_recv",
 		Acls:           make(map[string]*types.Acl),
@@ -143,12 +146,31 @@ func New() *Context {
 		Variables:      predefinedVariables(),
 		RegexVariables: newRegexMatchedValues(),
 	}
+
+	for i := range opts {
+		opts[i](c)
+	}
+	return c
 }
 
 func (c *Context) Mode() int {
 	return c.curMode
 }
 
+func (c *Context) Resolver() resolver.Resolver {
+	if c.resolver == nil {
+		c.resolver = &resolver.EmptyResolver{}
+	}
+	return c.resolver
+}
+
+func (c *Context) Snippets() *FastlySnippet {
+	if c.fastlySnippets == nil {
+		c.fastlySnippets = &FastlySnippet{}
+	}
+	return c.fastlySnippets
+}
+
 func (c *Context) CurrentFunction() string {
 	return c.curName
 }
diff --git a/context/option.go b/context/option.go
new file mode 100644
index 0000000..35c98c5
--- /dev/null
+++ b/context/option.go
@@ -0,0 +1,19 @@
+package context
+
+import (
+	"github.com/ysugimoto/falco/resolver"
+)
+
+type Option func(c *Context)
+
+func WithResolver(rslv resolver.Resolver) Option {
+	return func(c *Context) {
+		c.resolver = rslv
+	}
+}
+
+func WithFastlySnippets(fs *FastlySnippet) Option {
+	return func(c *Context) {
+		c.fastlySnippets = fs
+	}
+}
diff --git a/context/snippet.go b/context/snippet.go
new file mode 100644
index 0000000..f14c2b4
--- /dev/null
+++ b/context/snippet.go
@@ -0,0 +1,19 @@
+package context
+
+type FastlySnippetItem struct {
+	Data string
+	Name string
+}
+
+type FastlySnippet struct {
+	Dictionaries []FastlySnippetItem
+	Acls         []FastlySnippetItem
+	Backends     []FastlySnippetItem
+}
+
+func (f *FastlySnippet) EmbedSnippets() []FastlySnippetItem {
+	return append(
+		f.Dictionaries,
+		append(f.Acls, f.Backends...)...,
+	)
+}
diff --git a/examples/default05.vcl b/examples/default05.vcl
new file mode 100644
index 0000000..7f451a0
--- /dev/null
+++ b/examples/default05.vcl
@@ -0,0 +1,41 @@
+backend httpbin_org {
+  .connect_timeout = 1s;
+  .dynamic = true;
+  .port = "443";
+  .host = "httpbin.org";
+  .first_byte_timeout = 20s;
+  .max_connections = 500;
+  .between_bytes_timeout = 20s;
+  .share_key = "xei5lohleex3Joh5ie5uy7du";
+  .ssl = true;
+  .ssl_sni_hostname = "httpbin.org";
+  .ssl_cert_hostname = "httpbin.org";
+  .ssl_check_cert = always;
+  .min_tls_version = "1.2";
+  .max_tls_version = "1.2";
+  .probe = {
+    .request = "GET /status/200 HTTP/1.1" "Host: httpbin.org" "Connection: close";
+    .dummy = true;
+    .threshold = 1;
+    .window = 2;
+    .timeout = 5s;
+    .initial = 1;
+    .expected_response = 200;
+    .interval = 10s;
+  }
+}
+
+include "default05_include01";
+
+sub vcl_recv {
+
+  #Fastly recv
+  set req.backend = httpbin_org;
+
+  if (req.http.Some-Truthy-Header) {
+    #include "default05_include02";
+  }
+
+  return (pass);
+}
+
diff --git a/examples/default05_include01.vcl b/examples/default05_include01.vcl
new file mode 100644
index 0000000..9e89bac
--- /dev/null
+++ b/examples/default05_include01.vcl
@@ -0,0 +1,3 @@
+sub included_subroutine {
+  set req.http.Included = "1";
+}
diff --git a/examples/default05_include02.vcl b/examples/default05_include02.vcl
new file mode 100644
index 0000000..b866631
--- /dev/null
+++ b/examples/default05_include02.vcl
@@ -0,0 +1,1 @@
+set req.http.Included-In-If-Statement = "1";
diff --git a/linter/errors.go b/linter/errors.go
index 3195fcf..7ffcb4a 100644
--- a/linter/errors.go
+++ b/linter/errors.go
@@ -5,6 +5,7 @@ import (
 	"strings"
 
 	"github.com/ysugimoto/falco/ast"
+	"github.com/ysugimoto/falco/lexer"
 	"github.com/ysugimoto/falco/token"
 	"github.com/ysugimoto/falco/types"
 )
@@ -347,3 +348,8 @@ func ProtectedHTTPHeader(m *ast.Meta, name string) *LintError {
 		Message:  fmt.Sprintf("%s HTTP header could not be modified in VCL", name),
 	}
 }
+
+type FatalError struct {
+	Lexer *lexer.Lexer
+	Error error
+}
diff --git a/linter/linter.go b/linter/linter.go
index 6863ed5..86a3f3a 100644
--- a/linter/linter.go
+++ b/linter/linter.go
@@ -5,19 +5,30 @@ import (
 	"net"
 	"strings"
 
+	"github.com/pkg/errors"
 	regexp "github.com/shadialtarsha/go-pcre"
 	"github.com/ysugimoto/falco/ast"
 	"github.com/ysugimoto/falco/context"
+	"github.com/ysugimoto/falco/lexer"
+	"github.com/ysugimoto/falco/parser"
 	"github.com/ysugimoto/falco/token"
 	"github.com/ysugimoto/falco/types"
 )
 
 type Linter struct {
-	Errors []error
+	Errors         []error
+	FatalError     *FatalError
+	includexLexers map[string]*lexer.Lexer
 }
 
 func New() *Linter {
-	return &Linter{}
+	return &Linter{
+		includexLexers: make(map[string]*lexer.Lexer),
+	}
+}
+
+func (l *Linter) Lexers() map[string]*lexer.Lexer {
+	return l.includexLexers
 }
 
 func (l *Linter) Error(err error) {
@@ -248,9 +259,12 @@ 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)
+
 	// https://github.com/ysugimoto/falco/issues/50
 	// To support subroutine hoisting, add root statements to context firstly and lint each statements after that.
-	statements := l.factoryRootStatements(vcl, ctx)
+	statements = l.factoryRootDeclarations(statements, ctx)
 
 	// Lint each statement/declaration logics
 	for _, s := range statements {
@@ -260,10 +274,54 @@ 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 {
+	var resolved []ast.Statement
+
+	for _, stmt := range statements {
+		include, ok := stmt.(*ast.IncludeStatement)
+		if !ok {
+			resolved = append(resolved, stmt)
+			continue
+		}
+
+		// Check snippet inclusion
+		if strings.HasPrefix(include.Module.Value, "snippet::") {
+			// TODO: implement fastly managed snippet inclusion
+			continue
+		}
+
+		// 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
+		}
+		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
+		}
+		resolved = append(resolved, l.resolveIncludeStatements(vcl.Statements, ctx)...)
+	}
+
+	return resolved
+}
+
 //nolint:gocognit,funlen
-func (l *Linter) factoryRootStatements(vcl *ast.VCL, ctx *context.Context) []ast.Statement {
-	var statements []ast.Statement
-	for _, stmt := range vcl.Statements {
+func (l *Linter) factoryRootDeclarations(statements []ast.Statement, ctx *context.Context) []ast.Statement {
+	var factory []ast.Statement
+	for _, stmt := range statements {
 		switch t := stmt.(type) {
 		case *ast.AclDeclaration:
 			if err := ctx.AddAcl(t.Name.Value, &types.Acl{Decl: t}); err != nil {
@@ -274,7 +332,7 @@ func (l *Linter) factoryRootStatements(vcl *ast.VCL, ctx *context.Context) []ast
 				}
 				l.Error(e.Match(ACL_DUPLICATED))
 			}
-			statements = append(statements, stmt)
+			factory = append(factory, stmt)
 		case *ast.BackendDeclaration:
 			if err := ctx.AddBackend(t.Name.Value, &types.Backend{BackendDecl: t}); err != nil {
 				e := &LintError{
@@ -284,10 +342,9 @@ func (l *Linter) factoryRootStatements(vcl *ast.VCL, ctx *context.Context) []ast
 				}
 				l.Error(e.Match(BACKEND_DUPLICATED))
 			}
-			statements = append(statements, stmt)
+			factory = append(factory, stmt)
 		case *ast.ImportStatement:
-			continue
-		case *ast.IncludeStatement:
+			// @ysugimoto skipped. import statement no longer used?
 			continue
 		case *ast.DirectorDeclaration:
 			if err := ctx.AddDirector(t.Name.Value, &types.Director{Decl: t}); err != nil {
@@ -298,7 +355,7 @@ func (l *Linter) factoryRootStatements(vcl *ast.VCL, ctx *context.Context) []ast
 				}
 				l.Error(e.Match(DIRECTOR_DUPLICATED))
 			}
-			statements = append(statements, stmt)
+			factory = append(factory, stmt)
 		case *ast.TableDeclaration:
 			table := &types.Table{
 				Decl:       t,
@@ -328,7 +385,7 @@ func (l *Linter) factoryRootStatements(vcl *ast.VCL, ctx *context.Context) []ast
 				}
 				l.Error(e.Match(TABLE_DUPLICATED))
 			}
-			statements = append(statements, stmt)
+			factory = append(factory, stmt)
 		case *ast.SubroutineDeclaration:
 			if t.ReturnType != nil {
 				if context.IsFastlySubroutine(t.Name.Value) {
@@ -369,7 +426,7 @@ func (l *Linter) factoryRootStatements(vcl *ast.VCL, ctx *context.Context) []ast
 					l.Error(e.Match(SUBROUTINE_DUPLICATED))
 				}
 			}
-			statements = append(statements, stmt)
+			factory = append(factory, stmt)
 		case *ast.PenaltyboxDeclaration:
 			if err := ctx.AddPenaltybox(t.Name.Value, &types.Penaltybox{Decl: t}); err != nil {
 				e := &LintError{
@@ -379,7 +436,7 @@ func (l *Linter) factoryRootStatements(vcl *ast.VCL, ctx *context.Context) []ast
 				}
 				l.Error(e.Match(PENALTYBOX_DUPLICATED))
 			}
-			statements = append(statements, stmt)
+			factory = append(factory, stmt)
 		case *ast.RatecounterDeclaration:
 			if err := ctx.AddRatecounter(t.Name.Value, &types.Ratecounter{Decl: t}); err != nil {
 				e := &LintError{
@@ -389,12 +446,12 @@ func (l *Linter) factoryRootStatements(vcl *ast.VCL, ctx *context.Context) []ast
 				}
 				l.Error(e.Match(SUBROUTINE_DUPLICATED))
 			}
-			statements = append(statements, stmt)
+			factory = append(factory, stmt)
 		default:
 			l.Error(fmt.Errorf("unexpected statement declaration found: %s", t.String()))
 		}
 	}
-	return statements
+	return factory
 }
 
 func (l *Linter) lintAclDeclaration(decl *ast.AclDeclaration) types.Type {
@@ -789,7 +846,8 @@ 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 {
+	statements := l.resolveIncludeStatements(block.Statements, ctx)
+	for _, stmt := range statements {
 		l.lint(stmt, ctx)
 	}
 
diff --git a/parser/statement_parser.go b/parser/statement_parser.go
index 1c17bfb..26f0d14 100644
--- a/parser/statement_parser.go
+++ b/parser/statement_parser.go
@@ -101,6 +101,8 @@ func (p *Parser) parseBlockStatement() (*ast.BlockStatement, error) {
 			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) {
diff --git a/resolver/empty.go b/resolver/empty.go
new file mode 100644
index 0000000..e3df18a
--- /dev/null
+++ b/resolver/empty.go
@@ -0,0 +1,18 @@
+package resolver
+
+import (
+	"github.com/pkg/errors"
+	"github.com/ysugimoto/falco/ast"
+)
+
+type EmptyResolver struct{}
+
+func (e *EmptyResolver) MainVCL() (*VCL, error) {
+	return nil, errors.New("Empty Resolver returns error")
+}
+
+func (e *EmptyResolver) Resolve(stmt *ast.IncludeStatement) (*VCL, error) {
+	return nil, errors.New("Empty Resolver returns error")
+}
+
+func (e *EmptyResolver) Name() string { return "__EMPTY__" }
diff --git a/cmd/falco/resolver_file.go b/resolver/file.go
similarity index 77%
rename from cmd/falco/resolver_file.go
rename to resolver/file.go
index f6c6bc6..9f34c57 100644
--- a/cmd/falco/resolver_file.go
+++ b/resolver/file.go
@@ -1,4 +1,4 @@
-package main
+package resolver
 
 import (
 	"bytes"
@@ -9,6 +9,7 @@ import (
 	"path/filepath"
 
 	"github.com/pkg/errors"
+	"github.com/ysugimoto/falco/ast"
 )
 
 // FileResolver is filesystem resolver, basically used for built vcl files
@@ -17,7 +18,7 @@ type FileResolver struct {
 	includePaths []string
 }
 
-func NewFileResolvers(main string, c *Config) ([]Resolver, error) {
+func NewFileResolvers(main string, includePaths []string) ([]Resolver, error) {
 	if main == "" {
 		return nil, ErrEmptyMain
 	}
@@ -34,20 +35,20 @@ func NewFileResolvers(main string, c *Config) ([]Resolver, error) {
 		return nil, errors.New(fmt.Sprintf("Failed to get absolute path: %s", err.Error()))
 	}
 
-	var includePaths []string
+	var ips []string
 	// Add include paths as absolute
-	for i := range c.IncludePaths {
-		p, err := filepath.Abs(c.IncludePaths[i])
+	for i := range includePaths {
+		p, err := filepath.Abs(includePaths[i])
 		if err == nil {
-			includePaths = append(includePaths, p)
+			ips = append(ips, p)
 		}
 	}
-	includePaths = append(includePaths, filepath.Dir(abs))
+	ips = append(ips, filepath.Dir(abs))
 
 	return []Resolver{
 		&FileResolver{
 			main:         abs,
-			includePaths: includePaths,
+			includePaths: ips,
 		},
 	}, nil
 }
@@ -82,8 +83,8 @@ func (f *FileResolver) MainVCL() (*VCL, error) {
 	return f.getVCL(f.main)
 }
 
-func (f *FileResolver) Resolve(module string) (*VCL, error) {
-	modulePathWithExtension := module
+func (f *FileResolver) Resolve(stmt *ast.IncludeStatement) (*VCL, error) {
+	modulePathWithExtension := stmt.Module.Value
 	if !strings.HasSuffix(modulePathWithExtension, ".vcl") {
 		modulePathWithExtension += ".vcl"
 	}
@@ -95,5 +96,5 @@ func (f *FileResolver) Resolve(module string) (*VCL, error) {
 		}
 	}
 
-	return nil, errors.New(fmt.Sprintf("Failed to resolve include file: %s.vcl", module))
+	return nil, errors.New(fmt.Sprintf("Failed to resolve include file: %s", modulePathWithExtension))
 }
diff --git a/cmd/falco/resolver.go b/resolver/resolver.go
similarity index 66%
rename from cmd/falco/resolver.go
rename to resolver/resolver.go
index dfee329..0a861ae 100644
--- a/cmd/falco/resolver.go
+++ b/resolver/resolver.go
@@ -1,7 +1,8 @@
-package main
+package resolver
 
 import (
 	"github.com/pkg/errors"
+	"github.com/ysugimoto/falco/ast"
 )
 
 var (
@@ -17,6 +18,8 @@ type VCL struct {
 // from various sources e.g. file or JSON (terraform planned data)
 type Resolver interface {
 	MainVCL() (*VCL, error)
-	Resolve(module string) (*VCL, error)
+	Resolve(stmt *ast.IncludeStatement) (*VCL, error)
 	Name() string
+	// TODO: implement
+	// ResolveScopeSnippet(scope string) (*VCL, error)
 }
diff --git a/cmd/falco/resolver_terraform.go b/resolver/terraform.go
similarity index 71%
rename from cmd/falco/resolver_terraform.go
rename to resolver/terraform.go
index 2d159ca..142d8be 100644
--- a/cmd/falco/resolver_terraform.go
+++ b/resolver/terraform.go
@@ -1,9 +1,11 @@
-package main
+package resolver
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/pkg/errors"
+	"github.com/ysugimoto/falco/ast"
 	"github.com/ysugimoto/falco/terraform"
 )
 
@@ -46,12 +48,14 @@ func (s *TerraformResolver) MainVCL() (*VCL, error) {
 	return s.Main, nil
 }
 
-func (s *TerraformResolver) Resolve(module string) (*VCL, error) {
+func (s *TerraformResolver) Resolve(stmt *ast.IncludeStatement) (*VCL, error) {
+	modulePathWithoutExtension := strings.TrimSuffix(stmt.Module.Value, ".vcl")
+
 	for i := range s.Modules {
-		if s.Modules[i].Name == module {
+		if s.Modules[i].Name == modulePathWithoutExtension {
 			return s.Modules[i], nil
 		}
 	}
 
-	return nil, errors.New(fmt.Sprintf("Failed to resolve include file: %s.vcl", module))
+	return nil, errors.New(fmt.Sprintf("Failed to resolve include module: %s", modulePathWithoutExtension))
 }


Test patch:
diff --git a/cmd/falco/runner_test.go b/cmd/falco/runner_test.go
index 380268e..1bef4b9 100644
--- a/cmd/falco/runner_test.go
+++ b/cmd/falco/runner_test.go
@@ -1,41 +1,14 @@
 package main
 
 import (
-	"errors"
 	"os"
 	"testing"
 
+	"github.com/ysugimoto/falco/resolver"
 	"github.com/ysugimoto/falco/terraform"
 )
 
-type mockResolver struct {
-	dependency map[string]string
-	main       string
-}
-
-func (m *mockResolver) MainVCL() (*VCL, error) {
-	return &VCL{
-		Name: "main.vcl",
-		Data: m.main,
-	}, nil
-}
-
-func (m *mockResolver) Resolve(module string) (*VCL, error) {
-	if v, ok := m.dependency[module]; !ok {
-		return nil, errors.New(module + " is not defined")
-	} else {
-		return &VCL{
-			Name: module + ".vcl",
-			Data: v,
-		}, nil
-	}
-}
-
-func (m *mockResolver) Name() string {
-	return ""
-}
-
-func loadFromTfJson(fileName string, t *testing.T) ([]Resolver, Fetcher) {
+func loadFromTfJson(fileName string, t *testing.T) ([]resolver.Resolver, Fetcher) {
 	buf, err := os.ReadFile(fileName)
 	if err != nil {
 		t.Fatalf("Unexpected error %s reading file %s ", fileName, err)
@@ -46,7 +19,7 @@ func loadFromTfJson(fileName string, t *testing.T) ([]Resolver, Fetcher) {
 		t.Fatalf("Unexpected error %s unarshalling %s ", fileName, err)
 	}
 
-	rslv := NewTerraformResolver(services)
+	rslv := resolver.NewTerraformResolver(services)
 	f := terraform.NewTerraformFetcher(services)
 	return rslv, f
 }
@@ -54,12 +27,12 @@ func loadFromTfJson(fileName string, t *testing.T) ([]Resolver, Fetcher) {
 func TestResolveExternalWithExternalProperties(t *testing.T) {
 	for _, fileName := range []string{"../../terraform/data/terraform-valid.json", "../../terraform/data/terraform-valid-weird-name.json"} {
 		rslv, f := loadFromTfJson(fileName, t)
-		r, err := NewRunner(rslv[0], &Config{V: true}, f)
+		r, err := NewRunner(&Config{V: true}, f)
 		if err != nil {
 			t.Fatalf("Unexpected runner creation error: %s", err)
 			return
 		}
-		ret, err := r.Run()
+		ret, err := r.Run(rslv[0])
 		if err != nil {
 			t.Fatalf("Unexpected Run() error: %s", err)
 		}
@@ -78,12 +51,12 @@ func TestResolveExternalWithExternalProperties(t *testing.T) {
 func TestResolveExternalWithNoExternalProperties(t *testing.T) {
 	fileName := "../../terraform/data/terraform-empty.json"
 	rslv, f := loadFromTfJson(fileName, t)
-	r, err := NewRunner(rslv[0], &Config{V: true}, f)
+	r, err := NewRunner(&Config{V: true}, f)
 	if err != nil {
 		t.Fatalf("Unexpected runner creation error: %s", err)
 		return
 	}
-	ret, err := r.Run()
+	ret, err := r.Run(rslv[0])
 	if err != nil {
 		t.Fatalf("Unexpected Run() error: %s", err)
 	}
@@ -101,11 +74,11 @@ func TestResolveExternalWithNoExternalProperties(t *testing.T) {
 func TestResolveWithDuplicateDeclarations(t *testing.T) {
 	fileName := "../../terraform/data/terraform-duplicate.json"
 	rslv, f := loadFromTfJson(fileName, t)
-	r, err := NewRunner(rslv[0], &Config{V: true}, f)
+	r, err := NewRunner(&Config{V: true}, f)
 	if err != nil {
 		t.Fatalf("Unexpected runner creation error: %s", err)
 	}
-	ret, err := r.Run()
+	ret, err := r.Run(rslv[0])
 	if err != nil {
 		t.Fatalf("Unexpected Run() error: %s", err)
 	}
@@ -115,132 +88,6 @@ func TestResolveWithDuplicateDeclarations(t *testing.T) {
 	}
 }
 
-func TestResolveIncludeStatement(t *testing.T) {
-	mock := &mockResolver{
-		dependency: map[string]string{
-			"deps01": `
-sub foo {
-	set req.backend = httpbin_org;
-}
-
-sub bar {
-	set req.http.Foo = "bar";
-}
-			`,
-		},
-		main: `
-backend httpbin_org {
-  .connect_timeout = 1s;
-  .dynamic = true;
-  .port = "443";
-  .host = "httpbin.org";
-  .first_byte_timeout = 20s;
-  .max_connections = 500;
-  .between_bytes_timeout = 20s;
-  .share_key = "xei5lohleex3Joh5ie5uy7du";
-  .ssl = true;
-  .ssl_sni_hostname = "httpbin.org";
-  .ssl_cert_hostname = "httpbin.org";
-  .ssl_check_cert = always;
-  .min_tls_version = "1.2";
-  .max_tls_version = "1.2";
-}
-
-include "deps01";
-
-sub vcl_recv {
-   #FASTLY RECV
-   call foo;
-}
-		`,
-	}
-	r, err := NewRunner(mock, &Config{V: true}, nil)
-	if err != nil {
-		t.Errorf("Unexpected runner creation error: %s", err)
-		return
-	}
-	ret, err := r.Run()
-	if err != nil {
-		t.Errorf("Unexpected Run() error: %s", err)
-		return
-	}
-	if ret.Infos > 0 {
-		t.Errorf("Infos expects 0, got %d", ret.Infos)
-	}
-	// Above VCL should have one warning that subroutine "bar" is not defined
-	if ret.Warnings != 1 {
-		t.Errorf("Warning expects 0, got %d", ret.Warnings)
-	}
-	if ret.Errors > 0 {
-		t.Errorf("Errors expects 0, got %d", ret.Errors)
-	}
-	if len(ret.Vcl.AST.Statements) != 4 {
-		t.Errorf("Parsed VCL should have 4 statements, got %d", len(ret.Vcl.AST.Statements))
-	}
-}
-
-func TestResolveNestedIncludeStatement(t *testing.T) {
-	mock := &mockResolver{
-		dependency: map[string]string{
-			"deps01": `
-include "deps02";
-			`,
-			"deps02": `
-sub foo {
-	set req.backend = httpbin_org;
-}
-			`,
-		},
-		main: `
-backend httpbin_org {
-  .connect_timeout = 1s;
-  .dynamic = true;
-  .port = "443";
-  .host = "httpbin.org";
-  .first_byte_timeout = 20s;
-  .max_connections = 500;
-  .between_bytes_timeout = 20s;
-  .share_key = "xei5lohleex3Joh5ie5uy7du";
-  .ssl = true;
-  .ssl_sni_hostname = "httpbin.org";
-  .ssl_cert_hostname = "httpbin.org";
-  .ssl_check_cert = always;
-  .min_tls_version = "1.2";
-  .max_tls_version = "1.2";
-}
-
-include "deps01";
-
-sub vcl_recv {
-   #FASTLY RECV
-   call foo;
-}
-		`,
-	}
-	r, err := NewRunner(mock, &Config{V: true}, nil)
-	if err != nil {
-		t.Errorf("Unexpected runner creation error: %s", err)
-		return
-	}
-	ret, err := r.Run()
-	if err != nil {
-		t.Errorf("Unexpected Run() error: %s", err)
-		return
-	}
-	if ret.Infos > 0 {
-		t.Errorf("Infos expects 0, got %d", ret.Infos)
-	}
-	if ret.Warnings > 0 {
-		t.Errorf("Warning expects 0, got %d", ret.Warnings)
-	}
-	if ret.Errors > 0 {
-		t.Errorf("Errors expects 0, got %d", ret.Errors)
-	}
-	if len(ret.Vcl.AST.Statements) != 3 {
-		t.Errorf("Parsed VCL should have 3 statements, got %d", len(ret.Vcl.AST.Statements))
-	}
-}
-
 // Adds a test for all the example code in the repo to make sure we don't accidentally
 // break those as they are the first thing someone might try on the repo.
 func TestRepositoryExamples(t *testing.T) {
@@ -284,17 +131,17 @@ func TestRepositoryExamples(t *testing.T) {
 	c := &Config{V: true}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			resolvers, err := NewFileResolvers(tt.fileName, c)
+			resolvers, err := resolver.NewFileResolvers(tt.fileName, c.IncludePaths)
 			if err != nil {
 				t.Errorf("Unexpected runner creation error: %s", err)
 				return
 			}
-			r, err := NewRunner(resolvers[0], &Config{V: true}, nil)
+			r, err := NewRunner(&Config{V: true}, nil)
 			if err != nil {
 				t.Errorf("Unexpected runner creation error: %s", err)
 				return
 			}
-			ret, err := r.Run()
+			ret, err := r.Run(resolvers[0])
 			if tt.errors != 0 {
 				if err == nil {
 					t.Errorf("Expected Run() to generate an error")
diff --git a/linter/linter_test.go b/linter/linter_test.go
index 7e27f6f..63a1443 100644
--- a/linter/linter_test.go
+++ b/linter/linter_test.go
@@ -4,13 +4,16 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/pkg/errors"
+	"github.com/ysugimoto/falco/ast"
 	"github.com/ysugimoto/falco/context"
 	"github.com/ysugimoto/falco/lexer"
 	"github.com/ysugimoto/falco/parser"
+	"github.com/ysugimoto/falco/resolver"
 	"github.com/ysugimoto/falco/types"
 )
 
-func assertNoError(t *testing.T, input string) {
+func assertNoError(t *testing.T, input string, opts ...context.Option) {
 	vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
 	if err != nil {
 		t.Errorf("unexpected parser error: %s", err)
@@ -18,13 +21,13 @@ func assertNoError(t *testing.T, input string) {
 	}
 
 	l := New()
-	l.lint(vcl, context.New())
+	l.lint(vcl, context.New(opts...))
 	if len(l.Errors) > 0 {
 		t.Errorf("Lint error: %s", l.Errors)
 	}
 }
 
-func assertError(t *testing.T, input string) {
+func assertError(t *testing.T, input string, opts ...context.Option) {
 	vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
 	if err != nil {
 		t.Errorf("unexpected parser error: %s", err)
@@ -32,12 +35,12 @@ func assertError(t *testing.T, input string) {
 	}
 
 	l := New()
-	l.lint(vcl, context.New())
+	l.lint(vcl, context.New(opts...))
 	if len(l.Errors) == 0 {
 		t.Errorf("Expect one lint error but empty returned")
 	}
 }
-func assertErrorWithSeverity(t *testing.T, input string, severity Severity) {
+func assertErrorWithSeverity(t *testing.T, input string, severity Severity, opts ...context.Option) {
 	vcl, err := parser.New(lexer.NewFromString(input)).ParseVCL()
 	if err != nil {
 		t.Errorf("unexpected parser error: %s", err)
@@ -45,7 +48,7 @@ func assertErrorWithSeverity(t *testing.T, input string, severity Severity) {
 	}
 
 	l := New()
-	l.lint(vcl, context.New())
+	l.lint(vcl, context.New(opts...))
 	if len(l.Errors) == 0 {
 		t.Errorf("Expect one lint error but empty returned")
 	}
@@ -2354,3 +2357,134 @@ func TestLintProtectedHTTPHeaders(t *testing.T) {
 		})
 	}
 }
+
+// statement resolve tests
+
+type mockResolver struct {
+	dependency map[string]string
+	main       string
+}
+
+func (m *mockResolver) MainVCL() (*resolver.VCL, error) {
+	return &resolver.VCL{
+		Name: "main.vcl",
+		Data: m.main,
+	}, nil
+}
+
+func (m *mockResolver) Resolve(stmt *ast.IncludeStatement) (*resolver.VCL, error) {
+	if v, ok := m.dependency[stmt.Module.Value]; !ok {
+		return nil, errors.New(stmt.Module.Value + " is not defined")
+	} else {
+		return &resolver.VCL{
+			Name: stmt.Module.Value + ".vcl",
+			Data: v,
+		}, nil
+	}
+}
+
+func (m *mockResolver) Name() string {
+	return ""
+}
+
+func TestResolveRootIncludeStatement(t *testing.T) {
+	mock := &mockResolver{
+		dependency: map[string]string{
+			"deps01": `
+sub foo {
+	set req.backend = httpbin_org;
+}
+
+sub bar {
+	set req.http.Foo = "bar";
+}
+			`,
+		},
+	}
+	input := `
+backend httpbin_org {
+  .connect_timeout = 1s;
+  .dynamic = true;
+  .port = "443";
+  .host = "httpbin.org";
+  .first_byte_timeout = 20s;
+  .max_connections = 500;
+  .between_bytes_timeout = 20s;
+  .share_key = "xei5lohleex3Joh5ie5uy7du";
+  .ssl = true;
+  .ssl_sni_hostname = "httpbin.org";
+  .ssl_cert_hostname = "httpbin.org";
+  .ssl_check_cert = always;
+  .min_tls_version = "1.2";
+  .max_tls_version = "1.2";
+}
+
+include "deps01";
+
+sub vcl_recv {
+   #FASTLY RECV
+   call foo;
+}
+		`
+	assertNoError(t, input, context.WithResolver(mock))
+}
+
+func TestResolveNestedIncludeStatement(t *testing.T) {
+	mock := &mockResolver{
+		dependency: map[string]string{
+			"deps01": `
+include "deps02";
+			`,
+			"deps02": `
+sub foo {
+	set req.backend = httpbin_org;
+}
+			`,
+		},
+	}
+	input := `
+backend httpbin_org {
+  .connect_timeout = 1s;
+  .dynamic = true;
+  .port = "443";
+  .host = "httpbin.org";
+  .first_byte_timeout = 20s;
+  .max_connections = 500;
+  .between_bytes_timeout = 20s;
+  .share_key = "xei5lohleex3Joh5ie5uy7du";
+  .ssl = true;
+  .ssl_sni_hostname = "httpbin.org";
+  .ssl_cert_hostname = "httpbin.org";
+  .ssl_check_cert = always;
+  .min_tls_version = "1.2";
+  .max_tls_version = "1.2";
+}
+
+include "deps01";
+
+sub vcl_recv {
+   #FASTLY RECV
+   call foo;
+}
+		`
+	assertNoError(t, input, context.WithResolver(mock))
+}
+
+func TestResolveIncludeStateInIfStatement(t *testing.T) {
+	mock := &mockResolver{
+		dependency: map[string]string{
+			"deps01": `
+set req.http.Foo = "bar";
+			`,
+		},
+	}
+	input := `
+sub vcl_recv {
+   #FASTLY RECV
+   if (req.http.Is-Some-Truthy) {
+		include "deps01";
+   }
+}
+		`
+	assertNoError(t, input, context.WithResolver(mock))
+}