Repository: openllb/hlb
Pull request #203 (2020-11-11 19:28:12)

Add linter test and refactor cli command with LintInfo
- `opts` is common variable name for with options, so refactoring `FormatOptions` -> `FormatInfo`, etc.
- Add `linter/linter_test.go`
- Refactor linter CLI command to allow for external consumption.


Patch:
diff --git a/cmd/hlb/command/format.go b/cmd/hlb/command/format.go
index f8843df..a21a2e6 100644
--- a/cmd/hlb/command/format.go
+++ b/cmd/hlb/command/format.go
@@ -36,23 +36,23 @@ var formatCommand = &cli.Command{
 			}
 		}()
 
-		return Format(Context(), rs, FormatOptions{
+		return Format(Context(), rs, FormatInfo{
 			Write: c.Bool("write"),
 		})
 	},
 }
 
-type FormatOptions struct {
+type FormatInfo struct {
 	Write bool
 }
 
-func Format(ctx context.Context, rs []io.Reader, opts FormatOptions) error {
+func Format(ctx context.Context, rs []io.Reader, info FormatInfo) error {
 	modules, err := parser.ParseMultiple(ctx, rs)
 	if err != nil {
 		return err
 	}
 
-	if opts.Write {
+	if info.Write {
 		for i, mod := range modules {
 			filename := lexer.NameOfReader(rs[i])
 			if filename == "" {
diff --git a/cmd/hlb/command/lint.go b/cmd/hlb/command/lint.go
index 33927a4..a62cd60 100644
--- a/cmd/hlb/command/lint.go
+++ b/cmd/hlb/command/lint.go
@@ -1,8 +1,10 @@
 package command
 
 import (
+	"context"
 	"errors"
 	"fmt"
+	"io"
 	"io/ioutil"
 	"os"
 
@@ -32,52 +34,63 @@ var lintCommand = &cli.Command{
 		}
 		defer rc.Close()
 
-		ctx := diagnostic.WithSources(Context(), builtin.Sources())
-		mod, err := parser.Parse(ctx, rc)
-		if err != nil {
-			return err
-		}
+		return Lint(Context(), rc, LintInfo{
+			Fix: c.Bool("fix"),
+		})
 
-		err = checker.SemanticPass(mod)
-		if err != nil {
-			return err
-		}
+	},
+}
 
-		err = linter.Lint(ctx, mod, linter.WithRecursive())
-		if err != nil {
-			spans := diagnostic.Spans(err)
-			for _, span := range spans {
-				fmt.Fprintf(os.Stderr, "%s\n", span.Pretty(ctx))
+type LintInfo struct {
+	Fix bool
+}
+
+func Lint(ctx context.Context, r io.Reader, info LintInfo) error {
+	ctx = diagnostic.WithSources(ctx, builtin.Sources())
+	mod, err := parser.Parse(ctx, r)
+	if err != nil {
+		return err
+	}
+
+	err = checker.SemanticPass(mod)
+	if err != nil {
+		return err
+	}
+
+	err = linter.Lint(ctx, mod, linter.WithRecursive())
+	if err != nil {
+		spans := diagnostic.Spans(err)
+		for _, span := range spans {
+			fmt.Fprintf(os.Stderr, "%s\n", span.Pretty(ctx))
 
-				if c.Bool("fix") {
-					var em *errdefs.ErrModule
-					if errors.As(span, &em) {
-						filename := em.Module.Pos.Filename
-						info, err := os.Stat(filename)
-						if err != nil {
-							return err
-						}
+			if info.Fix {
+				var em *errdefs.ErrModule
+				if errors.As(span, &em) {
+					filename := em.Module.Pos.Filename
+					info, err := os.Stat(filename)
+					if err != nil {
+						return err
+					}
 
-						err = ioutil.WriteFile(filename, []byte(em.Module.String()), info.Mode())
-						if err != nil {
-							return err
-						}
+					err = ioutil.WriteFile(filename, []byte(em.Module.String()), info.Mode())
+					if err != nil {
+						return err
 					}
 				}
 			}
+		}
 
-			color := diagnostic.Color(ctx)
-			fmt.Fprint(os.Stderr, color.Sprintf(
-				color.Bold("\nRun %s to automatically fix lint errors.\n"),
-				color.Green(fmt.Sprintf("`hlb lint --fix %s`", mod.Pos.Filename)),
-			))
+		color := diagnostic.Color(ctx)
+		fmt.Fprint(os.Stderr, color.Sprintf(
+			color.Bold("\nRun %s to automatically fix lint errors.\n"),
+			color.Green(fmt.Sprintf("`hlb lint --fix %s`", mod.Pos.Filename)),
+		))
 
-			if c.Bool("fix") {
-				return nil
-			}
-			return errdefs.WithAbort(err, len(spans))
+		if info.Fix {
+			return nil
 		}
+		return errdefs.WithAbort(err, len(spans))
+	}
 
-		return checker.Check(mod)
-	},
+	return checker.Check(mod)
 }
diff --git a/cmd/hlb/command/module.go b/cmd/hlb/command/module.go
index 4ff0b3a..db8a210 100644
--- a/cmd/hlb/command/module.go
+++ b/cmd/hlb/command/module.go
@@ -46,7 +46,7 @@ var moduleVendorCommand = &cli.Command{
 			return err
 		}
 
-		return Vendor(ctx, cln, VendorOptions{
+		return Vendor(ctx, cln, VendorInfo{
 			Args:    c.Args().Slice(),
 			Targets: c.StringSlice("target"),
 			Tidy:    false,
@@ -64,7 +64,7 @@ var moduleTidyCommand = &cli.Command{
 			return err
 		}
 
-		return Vendor(ctx, cln, VendorOptions{
+		return Vendor(ctx, cln, VendorInfo{
 			Args: c.Args().Slice(),
 			Tidy: true,
 		})
@@ -87,27 +87,27 @@ var moduleTreeCommand = &cli.Command{
 			return err
 		}
 
-		return Tree(ctx, cln, TreeOptions{
+		return Tree(ctx, cln, TreeInfo{
 			Args: c.Args().Slice(),
 			Long: c.Bool("long"),
 		})
 	},
 }
 
-type VendorOptions struct {
+type VendorInfo struct {
 	Args    []string
 	Targets []string
 	Tidy    bool
 }
 
-func Vendor(ctx context.Context, cln *client.Client, opts VendorOptions) error {
-	rc, err := ModuleReadCloser(opts.Args)
+func Vendor(ctx context.Context, cln *client.Client, info VendorInfo) error {
+	rc, err := ModuleReadCloser(info.Args)
 	if err != nil {
 		if !os.IsNotExist(err) {
 			return err
 		}
 
-		rc, err = findVendoredModule(err, opts.Args[0])
+		rc, err = findVendoredModule(err, info.Args[0])
 		if err != nil {
 			return err
 		}
@@ -145,7 +145,7 @@ func Vendor(ctx context.Context, cln *client.Client, opts VendorOptions) error {
 	p.Go(func(ctx context.Context) error {
 		defer p.Release()
 		ctx = codegen.WithMultiWriter(ctx, p.MultiWriter())
-		return module.Vendor(ctx, cln, mod, opts.Targets, opts.Tidy)
+		return module.Vendor(ctx, cln, mod, info.Targets, info.Tidy)
 	})
 
 	return p.Wait()
@@ -194,13 +194,13 @@ func findVendoredModule(errNotExist error, name string) (io.ReadCloser, error) {
 	return os.Open(filepath.Join(matchedModules[0], module.ModuleFilename))
 }
 
-type TreeOptions struct {
+type TreeInfo struct {
 	Args []string
 	Long bool
 }
 
-func Tree(ctx context.Context, cln *client.Client, opts TreeOptions) error {
-	rc, err := ModuleReadCloser(opts.Args)
+func Tree(ctx context.Context, cln *client.Client, info TreeInfo) error {
+	rc, err := ModuleReadCloser(info.Args)
 	if err != nil {
 		return err
 	}
@@ -223,7 +223,7 @@ func Tree(ctx context.Context, cln *client.Client, opts TreeOptions) error {
 
 	var tree treeprint.Tree
 	if exist {
-		tree, err = module.NewTree(ctx, cln, mod, opts.Long)
+		tree, err = module.NewTree(ctx, cln, mod, info.Long)
 		if err != nil {
 			return err
 		}
@@ -238,7 +238,7 @@ func Tree(ctx context.Context, cln *client.Client, opts TreeOptions) error {
 
 			var err error
 			ctx = codegen.WithMultiWriter(ctx, p.MultiWriter())
-			tree, err = module.NewTree(ctx, cln, mod, opts.Long)
+			tree, err = module.NewTree(ctx, cln, mod, info.Long)
 			return err
 		})
 
diff --git a/parser/walk.go b/parser/walk.go
index 55bd798..c366689 100644
--- a/parser/walk.go
+++ b/parser/walk.go
@@ -66,6 +66,9 @@ func (w *walker) walk(node Node, v Visitor) {
 			w.walk(n.Comments, v)
 		}
 	case *ImportDecl:
+		if n.DeprecatedPath != nil {
+			w.walk(n.DeprecatedPath, v)
+		}
 		if n.Expr != nil {
 			w.walk(n.Expr, v)
 		}


Test patch:
diff --git a/linter/linter_test.go b/linter/linter_test.go
new file mode 100644
index 0000000..0e79e44
--- /dev/null
+++ b/linter/linter_test.go
@@ -0,0 +1,79 @@
+package linter
+
+import (
+	"context"
+	"strings"
+	"testing"
+
+	"github.com/lithammer/dedent"
+	"github.com/openllb/hlb/builtin"
+	"github.com/openllb/hlb/checker"
+	"github.com/openllb/hlb/diagnostic"
+	"github.com/openllb/hlb/errdefs"
+	"github.com/openllb/hlb/parser"
+	"github.com/stretchr/testify/require"
+)
+
+type testCase struct {
+	name  string
+	input string
+	fn    func(*parser.Module) error
+}
+
+func TestChecker_Check(t *testing.T) {
+	t.Parallel()
+
+	for _, tc := range []testCase{{
+		"empty",
+		`
+		import foo "./foo.hlb"
+		`,
+		func(mod *parser.Module) error {
+			return errdefs.WithDeprecated(
+				mod, parser.Find(mod, `"./foo.hlb"`).(*parser.StringLit),
+				`import path without keyword "from" is deprecated`,
+			)
+		},
+	}} {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			in := strings.NewReader(dedent.Dedent(tc.input))
+
+			ctx := diagnostic.WithSources(context.Background(), builtin.Sources())
+			mod, err := parser.Parse(ctx, in)
+			require.NoError(t, err)
+
+			err = checker.SemanticPass(mod)
+			require.NoError(t, err)
+
+			err = Lint(ctx, mod)
+
+			var expected error
+			if tc.fn != nil {
+				expected = tc.fn(mod)
+			}
+			validateError(t, ctx, expected, err, tc.name)
+		})
+	}
+}
+
+func validateError(t *testing.T, ctx context.Context, expected, actual error, name string) {
+	switch {
+	case expected == nil:
+		require.NoError(t, actual, name)
+	case actual == nil:
+		require.NotNil(t, actual, name)
+	default:
+		espans := diagnostic.Spans(expected)
+		aspans := diagnostic.Spans(actual)
+		require.Equal(t, len(espans), len(aspans))
+
+		for i := 0; i < len(espans); i++ {
+			epretty := espans[i].Pretty(ctx)
+			t.Logf("[Expected]\n%s", epretty)
+			apretty := aspans[i].Pretty(ctx)
+			t.Logf("[Actual]\n%s", apretty)
+			require.Equal(t, epretty, apretty, name)
+		}
+	}
+}