Skip to content

Commit d479b3d

Browse files
authored
create a sqlcmd style linter (#271)
1 parent 516213e commit d479b3d

File tree

19 files changed

+293
-12
lines changed

19 files changed

+293
-12
lines changed

.editorconfig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# top-most EditorConfig file
2+
root = true
3+
4+
[*.go]
5+
indent_style = tab
6+
# (Please don't specify an indent_size here; that has too many unintended consequences.)
7+
8+
# IDE0073: File header
9+
file_header_template = Copyright (c) Microsoft Corporation.\nLicensed under the MIT license.

.vscode/launch.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
"version": "0.2.0",
66
"configurations": [
77

8-
9-
108
{
119

1210
"name": "Attach to Process",
@@ -36,5 +34,14 @@
3634
"program": "${workspaceFolder}/cmd/modern",
3735
"args" : ["-S", "np:.", "-i", "${workspaceFolder}/cmd/sqlcmd/testdata/select100.sql"],
3836
},
37+
{
38+
"name" : "Run sqlcmdlinter",
39+
"type" : "go",
40+
"request" : "launch",
41+
"mode" : "auto",
42+
"program": "${workspaceFolder}/cmd/sqlcmd-linter",
43+
"args" : ["${workspaceFolder}/..."]
44+
45+
}
3946
]
4047
}

build/build.cmd

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11
@echo off
2+
3+
REM We get the value of the escape character by using PROMPT $E
4+
for /F "tokens=1,2 delims=#" %%a in ('"prompt #$H#$E# & echo on & for %%b in (1) do rem"') do (
5+
set "DEL=%%a"
6+
set "ESC=%%b"
7+
)
8+
setlocal
9+
SET RED=%ESC%[1;31m
10+
echo %RED%
11+
REM run the custom sqlcmd linter for code style enforcement
12+
REM using for/do instead of running it directly so the status code isn't checked by the shell.
13+
REM Once we are prepared to block the build with the linter we will move this step into a pipeline
14+
for /F "usebackq" %%l in (`go run cmd\sqlcmd-linter\main.go -test %~dp0../...`) DO echo %%l
15+
echo %ESC%[0m
16+
endlocal
217
REM Get Version Tag
318
for /f %%i in ('"git describe --tags --abbrev=0"') do set sqlcmdVersion=%%i
419

@@ -9,13 +24,14 @@ REM Generate NOTICE
924
if not exist %gopath%\bin\go-licenses.exe (
1025
go install github.com/google/go-licenses@latest
1126
)
12-
go-licenses report github.com/microsoft/go-sqlcmd/cmd/modern --template build\NOTICE.tpl --ignore github.com/microsoft > %~dp0notice.txt
27+
go-licenses report github.com/microsoft/go-sqlcmd/cmd/modern --template build\NOTICE.tpl --ignore github.com/microsoft > %~dp0notice.txt 2>nul
1328
copy %~dp0NOTICE.header + %~dp0notice.txt %~dp0..\NOTICE.md
1429
del %~dp0notice.txt
1530

1631
REM Generates all versions of sqlcmd in platform-specific folder
1732
setlocal
1833

1934
for /F "tokens=1-3 delims=," %%i in (%~dp0arch.txt) do set GOOS=%%i&set GOARCH=%%j&go build -o %~dp0..\%%i-%%j\%%k -ldflags="-X main.version=%sqlcmdVersion%" %~dp0..\cmd\modern
35+
endlocal
2036

2137

cmd/sqlcmd-linter/main.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package main
2+
3+
import (
4+
sqlcmdlinter "github.com/microsoft/go-sqlcmd/pkg/sqlcmd-linter"
5+
"golang.org/x/tools/go/analysis/multichecker"
6+
)
7+
8+
func main() {
9+
multichecker.Main(sqlcmdlinter.AssertAnalyzer, sqlcmdlinter.ImportsAnalyzer)
10+
}

cmd/sqlcmd/sqlcmd_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func newKong(t *testing.T, cli interface{}, options ...kong.Option) *kong.Kong {
2626
kong.NoDefaultHelp(),
2727
kong.Exit(func(int) {
2828
t.Helper()
29-
t.Fatalf("unexpected exit()")
29+
assert.Fail(t, "unexpected exit()")
3030
}),
3131
}, options...)
3232
parser, err := kong.New(cli, options...)

pkg/sqlcmd-linter/imports.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package sqlcmdlinter
5+
6+
import (
7+
"go/ast"
8+
"go/token"
9+
"strings"
10+
11+
"golang.org/x/tools/go/analysis"
12+
"golang.org/x/tools/go/analysis/passes/inspect"
13+
"golang.org/x/tools/go/ast/inspector"
14+
)
15+
16+
var ImportsAnalyzer = &analysis.Analyzer{
17+
Name: "importslint",
18+
Doc: "Require most external packages be imported only by internal packages",
19+
Requires: []*analysis.Analyzer{inspect.Analyzer},
20+
Run: runImports,
21+
}
22+
23+
var AllowedImports = map[string][]string{
24+
`"github.com/alecthomas/kong`: {`cmd/sqlcmd`, `pkg/sqlcmd`},
25+
`"github.com/golang-sql/sqlexp`: {`pkg/sqlcmd`},
26+
`"github.com/google/uuid`: {},
27+
`"github.com/peterh/liner`: {`pkg/console`},
28+
`"github.com/microsoft/go-mssqldb`: {},
29+
`"github.com/microsoft/go-sqlcmd`: {},
30+
`"github.com/spf13/cobra`: {`cmd/sqlcmd`, `cmd/modern`},
31+
`"github.com/spf13/viper`: {`cmd/sqlcmd`, `cmd/modern`},
32+
`"github.com/stretchr/testify`: {},
33+
}
34+
35+
func runImports(pass *analysis.Pass) (interface{}, error) {
36+
inspectorInstance := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
37+
nodeFilter := []ast.Node{(*ast.File)(nil)}
38+
inspectorInstance.Preorder(nodeFilter, func(n ast.Node) {
39+
40+
f := n.(*ast.File)
41+
fileName := pass.Fset.Position(f.Package).Filename
42+
isInternal := strings.Contains(fileName, `internal\`) || strings.Contains(fileName, `internal/`)
43+
for _, s := range f.Imports {
44+
if s.Path.Kind == token.STRING {
45+
pkg := s.Path.Value
46+
if isInternal {
47+
if !isValidInternalImport(pkg) {
48+
pass.Reportf(s.Pos(), "Internal packages should not import %s", pkg)
49+
}
50+
} else if !isValidExternalImport(pkg, fileName) {
51+
pass.Reportf(s.Pos(), "Non-internal packages should not import %s", pkg)
52+
}
53+
}
54+
}
55+
})
56+
57+
return nil, nil
58+
}
59+
60+
func isValidInternalImport(pkg string) bool {
61+
return !strings.HasPrefix(pkg, `"github.com/microsoft/go-sqlcmd/pkg`) && !strings.HasPrefix(pkg, `"github.com/microsoft/go-sqlcmd/cmd`)
62+
}
63+
64+
func isValidExternalImport(pkg string, filename string) bool {
65+
if strings.HasPrefix(pkg, `"github.com`) {
66+
for key, paths := range AllowedImports {
67+
if strings.HasPrefix(pkg, key) {
68+
if len(paths) == 0 {
69+
// any package can import it
70+
return true
71+
}
72+
for _, p := range paths {
73+
// canonicalize path to Linux separator for comparison
74+
path := strings.ReplaceAll(filename, `\`, `/`)
75+
if strings.Contains(path, p) {
76+
return true
77+
}
78+
}
79+
}
80+
}
81+
return false
82+
}
83+
return true
84+
}

pkg/sqlcmd-linter/imports_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package sqlcmdlinter
5+
6+
import (
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
11+
"golang.org/x/tools/go/analysis/analysistest"
12+
)
13+
14+
func TestImports(t *testing.T) {
15+
wd, err := os.Getwd()
16+
if err != nil {
17+
t.Errorf("Failed to get wd: %s", err)
18+
}
19+
analysistest.Run(t, filepath.Join(wd, `testdata`), ImportsAnalyzer, "imports_linter_tests/...")
20+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This directory structure exists to provide stub package implementations for the linter tests. The `analysistest` package replaces `$GOPATH` with the local file system path. We create these stubs so the linter test files can closely mimic our production code.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package chroma
2+
3+
var C = 1
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package sqlcmd
2+
3+
var S = 1

0 commit comments

Comments
 (0)