Skip to content

Commit 8899c64

Browse files
committed
Linter
1 parent 2e346e5 commit 8899c64

File tree

737 files changed

+3604
-176
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

737 files changed

+3604
-176
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,6 @@ gojsonnet.egg-info/
2727
/linter/jsonnet-lint/jsonnet-lint
2828
/tests_path.source
2929

30+
/jsonnet-lint
31+
3032
/builtin-benchmark-results

.golangci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ issues:
99
exclude-use-default: false
1010
exclude:
1111
- "should have a package comment, unless it's in another file for this package"
12+
- "the surrounding loop is unconditionally terminated"
1213
linters-settings:
1314
golint:
1415
min-confidence: 0

builtins.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,8 +1572,8 @@ var funcBuiltins = buildBuiltinMap([]builtin{
15721572
&binaryBuiltin{name: "objectFieldsEx", function: builtinObjectFieldsEx, params: ast.Identifiers{"obj", "hidden"}},
15731573
&ternaryBuiltin{name: "objectHasEx", function: builtinObjectHasEx, params: ast.Identifiers{"obj", "fname", "hidden"}},
15741574
&unaryBuiltin{name: "type", function: builtinType, params: ast.Identifiers{"x"}},
1575-
&unaryBuiltin{name: "char", function: builtinChar, params: ast.Identifiers{"x"}},
1576-
&unaryBuiltin{name: "codepoint", function: builtinCodepoint, params: ast.Identifiers{"x"}},
1575+
&unaryBuiltin{name: "char", function: builtinChar, params: ast.Identifiers{"n"}},
1576+
&unaryBuiltin{name: "codepoint", function: builtinCodepoint, params: ast.Identifiers{"str"}},
15771577
&unaryBuiltin{name: "ceil", function: builtinCeil, params: ast.Identifiers{"x"}},
15781578
&unaryBuiltin{name: "floor", function: builtinFloor, params: ast.Identifiers{"x"}},
15791579
&unaryBuiltin{name: "sqrt", function: builtinSqrt, params: ast.Identifiers{"x"}},
@@ -1587,9 +1587,9 @@ var funcBuiltins = buildBuiltinMap([]builtin{
15871587
&unaryBuiltin{name: "exp", function: builtinExp, params: ast.Identifiers{"x"}},
15881588
&unaryBuiltin{name: "mantissa", function: builtinMantissa, params: ast.Identifiers{"x"}},
15891589
&unaryBuiltin{name: "exponent", function: builtinExponent, params: ast.Identifiers{"x"}},
1590-
&binaryBuiltin{name: "pow", function: builtinPow, params: ast.Identifiers{"base", "exp"}},
1590+
&binaryBuiltin{name: "pow", function: builtinPow, params: ast.Identifiers{"x", "n"}},
15911591
&binaryBuiltin{name: "modulo", function: builtinModulo, params: ast.Identifiers{"x", "y"}},
1592-
&unaryBuiltin{name: "md5", function: builtinMd5, params: ast.Identifiers{"x"}},
1592+
&unaryBuiltin{name: "md5", function: builtinMd5, params: ast.Identifiers{"s"}},
15931593
&ternaryBuiltin{name: "substr", function: builtinSubstr, params: ast.Identifiers{"str", "from", "len"}},
15941594
&ternaryBuiltin{name: "splitLimit", function: builtinSplitLimit, params: ast.Identifiers{"str", "c", "maxsplits"}},
15951595
&ternaryBuiltin{name: "strReplace", function: builtinStrReplace, params: ast.Identifiers{"str", "from", "to"}},

linter/jsonnet-lint/BUILD.bazel renamed to cmd/jsonnet-lint/BUILD.bazel

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
33
go_library(
44
name = "go_default_library",
55
srcs = ["cmd.go"],
6-
importpath = "github.com/google/go-jsonnet/linter/jsonnet-lint",
6+
importpath = "github.com/google/go-jsonnet/cmd/jsonnet-lint",
77
visibility = ["//visibility:private"],
88
deps = [
99
"//:go_default_library",
10+
"//cmd/internal/cmd:go_default_library",
1011
"//linter:go_default_library",
12+
"@com_github_fatih_color//:go_default_library",
1113
],
1214
)
1315

cmd/jsonnet-lint/cmd.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"io/ioutil"
7+
"os"
8+
"path/filepath"
9+
10+
"github.com/fatih/color"
11+
"github.com/google/go-jsonnet/cmd/internal/cmd"
12+
"github.com/google/go-jsonnet/linter"
13+
14+
jsonnet "github.com/google/go-jsonnet"
15+
)
16+
17+
func version(o io.Writer) {
18+
fmt.Fprintf(o, "Jsonnet linter %s\n", jsonnet.Version())
19+
}
20+
21+
func usage(o io.Writer) {
22+
version(o)
23+
fmt.Fprintln(o)
24+
fmt.Fprintln(o, "jsonnet-lint {<option>} { <filename> }")
25+
fmt.Fprintln(o)
26+
fmt.Fprintln(o, "Available options:")
27+
fmt.Fprintln(o, " -h / --help This message")
28+
fmt.Fprintln(o, " -J / --jpath <dir> Specify an additional library search dir")
29+
fmt.Fprintln(o, " (right-most wins)")
30+
fmt.Fprintln(o, " --version Print version")
31+
fmt.Fprintln(o)
32+
fmt.Fprintln(o, "Environment variables:")
33+
fmt.Fprintln(o, " JSONNET_PATH is a colon (semicolon on Windows) separated list of directories")
34+
fmt.Fprintln(o, " added in reverse order before the paths specified by --jpath (i.e. left-most")
35+
fmt.Fprintln(o, " wins). E.g. these are equivalent:")
36+
fmt.Fprintln(o, " JSONNET_PATH=a:b jsonnet -J c -J d")
37+
fmt.Fprintln(o, " JSONNET_PATH=d:c:a:b jsonnet")
38+
fmt.Fprintln(o, " jsonnet -J b -J a -J c -J d")
39+
fmt.Fprintln(o)
40+
fmt.Fprintln(o, "In all cases:")
41+
fmt.Fprintln(o, " <filename> can be - (stdin)")
42+
fmt.Fprintln(o, " Multichar options are expanded e.g. -abc becomes -a -b -c.")
43+
fmt.Fprintln(o, " The -- option suppresses option processing for subsequent arguments.")
44+
fmt.Fprintln(o, " Note that since filenames and jsonnet programs can begin with -, it is")
45+
fmt.Fprintln(o, " advised to use -- if the argument is unknown, e.g. jsonnetfmt -- \"$FILENAME\".")
46+
fmt.Fprintln(o)
47+
fmt.Fprintln(o, "Exit code:")
48+
fmt.Fprintln(o, " 0 – If the file was checked no problems were found.")
49+
fmt.Fprintln(o, " 1 – If errors occured which prevented checking (e.g. specified file is missing).")
50+
fmt.Fprintln(o, " 2 – If problems were found.")
51+
52+
}
53+
54+
type config struct {
55+
// TODO(sbarzowski) Allow multiple root files checked at once for greater efficiency
56+
inputFile string
57+
evalJpath []string
58+
}
59+
60+
func makeConfig() config {
61+
return config{
62+
evalJpath: []string{},
63+
}
64+
}
65+
66+
type processArgsStatus int
67+
68+
const (
69+
processArgsStatusContinue = iota
70+
processArgsStatusSuccessUsage = iota
71+
processArgsStatusFailureUsage = iota
72+
processArgsStatusSuccess = iota
73+
processArgsStatusFailure = iota
74+
)
75+
76+
func processArgs(givenArgs []string, config *config, vm *jsonnet.VM) (processArgsStatus, error) {
77+
args := cmd.SimplifyArgs(givenArgs)
78+
remainingArgs := make([]string, 0, len(args))
79+
i := 0
80+
81+
for ; i < len(args); i++ {
82+
arg := args[i]
83+
if arg == "--" {
84+
// All subsequent args are not options.
85+
i++
86+
for ; i < len(args); i++ {
87+
remainingArgs = append(remainingArgs, args[i])
88+
}
89+
break
90+
} else if arg == "-h" || arg == "--help" {
91+
return processArgsStatusSuccessUsage, nil
92+
} else if arg == "-v" || arg == "--version" {
93+
version(os.Stdout)
94+
return processArgsStatusSuccess, nil
95+
} else if arg == "-J" || arg == "--jpath" {
96+
dir := cmd.NextArg(&i, args)
97+
if len(dir) == 0 {
98+
return processArgsStatusFailure, fmt.Errorf("-J argument was empty string")
99+
}
100+
if dir[len(dir)-1] != '/' {
101+
dir += "/"
102+
}
103+
config.evalJpath = append(config.evalJpath, dir)
104+
} else if len(arg) > 1 && arg[0] == '-' {
105+
return processArgsStatusFailure, fmt.Errorf("unrecognized argument: %s", arg)
106+
} else {
107+
remainingArgs = append(remainingArgs, arg)
108+
}
109+
}
110+
111+
if len(remainingArgs) == 0 {
112+
return processArgsStatusFailureUsage, fmt.Errorf("file not provided")
113+
}
114+
115+
if len(remainingArgs) > 1 {
116+
return processArgsStatusFailure, fmt.Errorf("only one file is allowed")
117+
}
118+
119+
config.inputFile = remainingArgs[0]
120+
return processArgsStatusContinue, nil
121+
}
122+
123+
func die(err error) {
124+
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err.Error())
125+
os.Exit(1)
126+
}
127+
128+
func main() {
129+
cmd.StartCPUProfile()
130+
defer cmd.StopCPUProfile()
131+
132+
vm := jsonnet.MakeVM()
133+
vm.ErrorFormatter.SetColorFormatter(color.New(color.FgRed).Fprintf)
134+
135+
config := makeConfig()
136+
jsonnetPath := filepath.SplitList(os.Getenv("JSONNET_PATH"))
137+
for i := len(jsonnetPath) - 1; i >= 0; i-- {
138+
config.evalJpath = append(config.evalJpath, jsonnetPath[i])
139+
}
140+
141+
status, err := processArgs(os.Args[1:], &config, vm)
142+
if err != nil {
143+
fmt.Fprintln(os.Stderr, "ERROR: "+err.Error())
144+
}
145+
switch status {
146+
case processArgsStatusContinue:
147+
break
148+
case processArgsStatusSuccessUsage:
149+
usage(os.Stdout)
150+
os.Exit(0)
151+
case processArgsStatusFailureUsage:
152+
if err != nil {
153+
fmt.Fprintln(os.Stderr, "")
154+
}
155+
usage(os.Stderr)
156+
os.Exit(1)
157+
case processArgsStatusSuccess:
158+
os.Exit(0)
159+
case processArgsStatusFailure:
160+
os.Exit(1)
161+
}
162+
163+
vm.Importer(&jsonnet.FileImporter{
164+
JPaths: config.evalJpath,
165+
})
166+
167+
inputFile, err := os.Open(config.inputFile)
168+
if err != nil {
169+
die(err)
170+
}
171+
data, err := ioutil.ReadAll(inputFile)
172+
if err != nil {
173+
die(err)
174+
}
175+
err = inputFile.Close()
176+
if err != nil {
177+
die(err)
178+
}
179+
180+
cmd.MemProfile()
181+
182+
_, err = jsonnet.SnippetToAST(config.inputFile, string(data))
183+
if err != nil {
184+
die(err)
185+
}
186+
187+
errorsFound := linter.LintSnippet(vm, os.Stderr, config.inputFile, string(data))
188+
if errorsFound {
189+
fmt.Fprintf(os.Stderr, "Problems found!\n")
190+
os.Exit(2)
191+
}
192+
}

linter/BUILD.bazel

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
1-
load("@io_bazel_rules_go//go:def.bzl", "go_library")
1+
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
22

33
go_library(
44
name = "go_default_library",
5-
srcs = [
6-
"find_variables.go",
7-
"linter.go",
8-
],
5+
srcs = ["linter.go"],
96
importpath = "github.com/google/go-jsonnet/linter",
107
visibility = ["//visibility:public"],
118
deps = [
9+
"//:go_default_library",
1210
"//ast:go_default_library",
1311
"//internal/errors:go_default_library",
1412
"//internal/parser:go_default_library",
13+
"//linter/internal/common:go_default_library",
14+
"//linter/internal/traversal:go_default_library",
15+
"//linter/internal/types:go_default_library",
16+
"//linter/internal/utils:go_default_library",
17+
"//linter/internal/variables:go_default_library",
18+
],
19+
)
20+
21+
go_test(
22+
name = "go_default_test",
23+
srcs = ["linter_test.go"],
24+
data = glob(["testdata/**"]),
25+
embed = [":go_default_library"],
26+
deps = [
27+
"//:go_default_library",
28+
"//internal/testutils:go_default_library",
1529
],
1630
)

linter/README.md

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,45 @@
11
# jsonnet-lint
22

3-
Very basic experimental, quick and a bit dirty linter. It's intended to become a full featured linter as it matures. PRs are very welcome!
3+
This is a linter for Jsonnet. It is alpha stage, but it should be already useful.
44

5-
Don't expect much from it now.
5+
## Features
66

7-
## Notes
7+
The linter detect the following kinds of issues:
8+
* "Type" problems, such as:
9+
∗ Accessing nonexistent fields
10+
∗ Calling a function with a wrong number of arguments or named arguments
11+
which do not match the parameters
12+
∗ Trying to call a value which is not a function
13+
∗ Trying to index a value which is not an object, array or a string
14+
* Unused variables
15+
* Endlessly looping constructs, which are always invalid, but often appear as a result of confusion about language semantics (e.g. local x = x + 1)
16+
* Anything that is statically detected during normal execution, such as syntax errors and undeclared variables.
817

9-
It currently runs on a desugared file. It has its good and bad sides.
10-
We should figure out what's the right way.
18+
## Usage
19+
20+
`jsonnet-lint [options] <filename>`
21+
22+
## Design
23+
24+
### Goals
25+
26+
The purpose of the linter is to aid development by providing quick and clear feedback about simple problems. With that in mind I defined the following goals:
27+
- It should find common problems, especially the kinds resulting from typos, trivial omissions and issues resulting from misunderstanding of the semantics.
28+
- It should find problems in the parts of code which are not reached by the tests (especially
29+
important due to the lazy evaluation).
30+
- It must be practical to use with the existing Jsonnet code, without any need for modification.
31+
- It must be fast enough so it is practical to always run the linter before execution during development. The overhead required to run the linter prior to running the program in real world conditions should be comparable with parsing and desugaring.
32+
- It must be conservative regarding the reported problems. False negatives are preferable to false positives. False positives are allowed as long as they relate to code which is going to be confusing for humans to read or if they can be worked around easily while preserving readability.
33+
- Its results must be stable, i.e. trivial changes such as changing the order of variables in local expressions should not change the result nontrivially.
34+
- It must preserve the abstractions. Validity of the definitions should not depend on their use. In particular calling functions with specific arguments or accessing fields of objects should not cause errors in their definitions.
35+
- It should be possible to explicitly silence individual errors, so that occasional acknowledged false positives do not distract the users. This is espcially important if a clean pass is enforced in Continuous Integration.
36+
37+
### Rules
38+
39+
The above goals naturally lead to the some more specific code-level rules which all analyses must obey:
40+
41+
- All expressions should be checked, even the provably dead code.
42+
- Always consider both branches of the `if` expression possible (even if the condition is trivially always true or always false).
43+
- Correctness of a function definition should not depend on how it is used. In particular
44+
when analyzing the definition assume that function arguments can take arbitrary values.
45+
- Correctness of an object definition should not depend on how it is used. In particular when analyzing the definition assume that the object may be part of an arbitrary inheritance chain

0 commit comments

Comments
 (0)