Skip to content

Commit 5fe0fe7

Browse files
authored
Merge pull request #90 from mxlint/feature/readfile
introduce mxlint.io functions for javascript rules
2 parents 8b51035 + edb4e1d commit 5fe0fe7

3 files changed

Lines changed: 714 additions & 3 deletions

File tree

lint/lint_javascript.go

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,136 @@ package lint
33
import (
44
"fmt"
55
"os"
6+
"path/filepath"
67
"strings"
78
"time"
89

910
"github.com/grafana/sobek"
1011
"gopkg.in/yaml.v3"
1112
)
1213

14+
// resolvePath resolves the given path relative to the working directory and validates
15+
// that it stays within the working directory. Returns the absolute path or an error.
16+
func resolvePath(pathArg string, workingDirectory string) (string, error) {
17+
// Resolve the path relative to working directory
18+
var fullPath string
19+
if filepath.IsAbs(pathArg) {
20+
fullPath = pathArg
21+
} else {
22+
fullPath = filepath.Join(workingDirectory, pathArg)
23+
}
24+
25+
// Convert both paths to absolute and clean them to resolve any ".." or "." components
26+
absFullPath, err := filepath.Abs(fullPath)
27+
if err != nil {
28+
return "", fmt.Errorf("failed to resolve path: %w", err)
29+
}
30+
absFullPath = filepath.Clean(absFullPath)
31+
32+
absWorkingDir, err := filepath.Abs(workingDirectory)
33+
if err != nil {
34+
return "", fmt.Errorf("failed to resolve working directory: %w", err)
35+
}
36+
absWorkingDir = filepath.Clean(absWorkingDir)
37+
38+
// Check that the resolved path is within the working directory
39+
if !strings.HasPrefix(absFullPath, absWorkingDir+string(filepath.Separator)) && absFullPath != absWorkingDir {
40+
return "", fmt.Errorf("path %q is outside working directory %q", pathArg, workingDirectory)
41+
}
42+
43+
return absFullPath, nil
44+
}
45+
46+
// setupJavascriptVM creates a new sobek VM with the mxlint object exposed.
47+
// The mxlint object provides utility functions for JavaScript rules:
48+
// - mxlint.io.readfile(path): Reads a file and returns its contents as a string.
49+
// The path is resolved relative to the workingDirectory.
50+
// - mxlint.io.listdir(path): Lists the contents of a directory and returns an array of filenames.
51+
// The path is resolved relative to the workingDirectory.
52+
// - mxlint.io.isdir(path): Returns true if the path is a directory, false otherwise.
53+
// The path is resolved relative to the workingDirectory.
54+
func setupJavascriptVM(workingDirectory string) *sobek.Runtime {
55+
vm := sobek.New()
56+
57+
// Create the mxlint object
58+
mxlint := vm.NewObject()
59+
vm.Set("mxlint", mxlint)
60+
61+
// Create the io sub-object
62+
io := vm.NewObject()
63+
mxlint.Set("io", io)
64+
65+
// Set the readfile function
66+
io.Set("readfile", func(call sobek.FunctionCall) sobek.Value {
67+
if len(call.Arguments) == 0 {
68+
panic(vm.NewGoError(fmt.Errorf("mxlint.io.readfile requires a file path argument")))
69+
}
70+
filepathArg := call.Argument(0).String()
71+
72+
absPath, err := resolvePath(filepathArg, workingDirectory)
73+
if err != nil {
74+
panic(vm.NewGoError(fmt.Errorf("mxlint.io.readfile: %w", err)))
75+
}
76+
77+
content, err := os.ReadFile(absPath)
78+
if err != nil {
79+
panic(vm.NewGoError(err))
80+
}
81+
return vm.ToValue(string(content))
82+
})
83+
84+
// Set the listdir function
85+
io.Set("listdir", func(call sobek.FunctionCall) sobek.Value {
86+
if len(call.Arguments) == 0 {
87+
panic(vm.NewGoError(fmt.Errorf("mxlint.io.listdir requires a directory path argument")))
88+
}
89+
dirpathArg := call.Argument(0).String()
90+
91+
absPath, err := resolvePath(dirpathArg, workingDirectory)
92+
if err != nil {
93+
panic(vm.NewGoError(fmt.Errorf("mxlint.io.listdir: %w", err)))
94+
}
95+
96+
entries, err := os.ReadDir(absPath)
97+
if err != nil {
98+
panic(vm.NewGoError(err))
99+
}
100+
101+
// Convert directory entries to a slice of names
102+
names := make([]string, len(entries))
103+
for i, entry := range entries {
104+
names[i] = entry.Name()
105+
}
106+
107+
return vm.ToValue(names)
108+
})
109+
110+
// Set the isdir function
111+
io.Set("isdir", func(call sobek.FunctionCall) sobek.Value {
112+
if len(call.Arguments) == 0 {
113+
panic(vm.NewGoError(fmt.Errorf("mxlint.io.isdir requires a path argument")))
114+
}
115+
pathArg := call.Argument(0).String()
116+
117+
absPath, err := resolvePath(pathArg, workingDirectory)
118+
if err != nil {
119+
panic(vm.NewGoError(fmt.Errorf("mxlint.io.isdir: %w", err)))
120+
}
121+
122+
info, err := os.Stat(absPath)
123+
if err != nil {
124+
if os.IsNotExist(err) {
125+
return vm.ToValue(false)
126+
}
127+
panic(vm.NewGoError(err))
128+
}
129+
130+
return vm.ToValue(info.IsDir())
131+
})
132+
133+
return vm
134+
}
135+
13136
func evalTestcase_Javascript(rulePath string, inputFilePath string, ruleNumber string, ignoreNoqa bool) (*Testcase, error) {
14137
ruleContent, _ := os.ReadFile(rulePath)
15138
log.Debugf("js file: \n%s", ruleContent)
@@ -48,15 +171,17 @@ func evalTestcase_Javascript(rulePath string, inputFilePath string, ruleNumber s
48171

49172
startTime := time.Now()
50173

51-
vm := sobek.New()
174+
// Use the directory containing the input file as the working directory
175+
workingDirectory := filepath.Dir(inputFilePath)
176+
vm := setupJavascriptVM(workingDirectory)
52177
_, err = vm.RunString(string(ruleContent))
53178
if err != nil {
54179
panic(err)
55180
}
56181

57182
ruleFunction, ok := sobek.AssertFunction(vm.Get("rule"))
58183
if !ok {
59-
panic("rule(...) function not found")
184+
panic("rule(...) function not found in rule file: " + rulePath)
60185
}
61186

62187
res, err := ruleFunction(sobek.Undefined(), vm.ToValue(data))

0 commit comments

Comments
 (0)