@@ -3,13 +3,136 @@ package lint
33import (
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+
13136func 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