@@ -11,131 +11,138 @@ const { TOKEN } = require('../../parser/raw-transfer/lazy-common.js'),
1111 { Visitor, getVisitorsArr } = require ( '../../parser/raw-transfer/visitor.js' ) ,
1212 walkProgram = require ( '../../parser/generated/lazy/walk.js' ) ;
1313
14- const textDecoder = new TextDecoder ( 'utf-8' , { ignoreBOM : true } ) ;
14+ // --------------------
15+ // Plugin loading
16+ // --------------------
1517
16- class PluginRegistry {
17- registeredPluginPaths = new Set ( ) ;
18+ // Absolute paths of plugins which have been loaded
19+ const registeredPluginPaths = new Set ( ) ;
1820
19- registeredRules = [ ] ;
21+ // Rule objects for loaded rules.
22+ // Indexed by `ruleId`, passed to `lintFile`.
23+ const registeredRules = [ ] ;
2024
21- isPluginRegistered ( path ) {
22- return this . registeredPluginPaths . has ( path ) ;
25+ /**
26+ * Load a plugin.
27+ *
28+ * Main logic is in separate function `loadPluginImpl`, because V8 cannot optimize functions
29+ * containing try/catch.
30+ *
31+ * @param {string } path - Absolute path of plugin file
32+ * @returns {string } - JSON result
33+ */
34+ async function loadPlugin ( path ) {
35+ try {
36+ return await loadPluginImpl ( path ) ;
37+ } catch ( error ) {
38+ const errorMessage = 'message' in error && typeof error . message === 'string'
39+ ? error . message
40+ : 'An unknown error occurred' ;
41+ return JSON . stringify ( { Failure : errorMessage } ) ;
2342 }
43+ }
2444
25- registerPlugin ( path , plugin ) {
26- // TODO: use a validation library to assert the shape of the plugin
27- this . registeredPluginPaths . add ( path ) ;
28- const ret = {
29- name : plugin . meta . name ,
30- offset : this . registeredRules . length ,
31- rules : [ ] ,
32- } ;
33-
34- for ( const [ ruleName , rule ] of Object . entries ( plugin . rules ) ) {
35- ret . rules . push ( ruleName ) ;
36- this . registeredRules . push ( rule ) ;
37- }
38-
39- return ret ;
45+ async function loadPluginImpl ( path ) {
46+ if ( registeredPluginPaths . has ( path ) ) {
47+ return JSON . stringify ( { Failure : 'This plugin has already been registered' } ) ;
4048 }
4149
42- * getRules ( ruleIds ) {
43- for ( const ruleId of ruleIds ) {
44- yield { rule : this . registeredRules [ ruleId ] , ruleId } ;
45- }
50+ const { default : plugin } = await import ( path ) ;
51+
52+ registeredPluginPaths . add ( path ) ;
53+
54+ // TODO: Use a validation library to assert the shape of the plugin, and of rules
55+ const rules = [ ] ;
56+ const ret = {
57+ name : plugin . meta . name ,
58+ offset : registeredRules . length ,
59+ rules,
60+ } ;
61+
62+ for ( const [ ruleName , rule ] of Object . entries ( plugin . rules ) ) {
63+ rules . push ( ruleName ) ;
64+ registeredRules . push ( rule ) ;
4665 }
66+
67+ return JSON . stringify ( { Success : ret } ) ;
4768}
4869
49- // Buffers cache
70+ // --------------------
71+ // Running rules
72+ // --------------------
73+
74+ // Buffers cache.
75+ //
76+ // All buffers sent from Rust are stored in this array, indexed by `bufferId` (also sent from Rust).
77+ // Buffers are only added to this array, never removed, so no buffers will be garbage collected
78+ // until the process exits.
5079const buffers = [ ] ;
5180
5281// Diagnostics array. Reused for every file.
5382const diagnostics = [ ] ;
5483
55- class Linter {
56- pluginRegistry = new PluginRegistry ( ) ;
57-
58- run ( ) {
59- return lint ( this . loadPlugin . bind ( this ) , this . lint . bind ( this ) ) ;
60- }
61-
62- /**
63- * @param {string } path - The absolute path of the plugin we're loading
64- */
65- async loadPlugin ( path ) {
66- if ( this . pluginRegistry . isPluginRegistered ( path ) ) {
67- return JSON . stringify ( {
68- Failure : 'This plugin has already been registered' ,
69- } ) ;
70- }
84+ // Text decoder, for decoding source text from buffer
85+ const textDecoder = new TextDecoder ( 'utf-8' , { ignoreBOM : true } ) ;
7186
72- try {
73- const { default : plugin } = await import ( path ) ;
74- const ret = this . pluginRegistry . registerPlugin ( path , plugin ) ;
75- return JSON . stringify ( { Success : ret } ) ;
76- } catch ( error ) {
77- const errorMessage = 'message' in error && typeof error . message === 'string'
78- ? error . message
79- : 'An unknown error occurred' ;
80- return JSON . stringify ( { Failure : errorMessage } ) ;
87+ // Run rules on a file.
88+ //
89+ // TODO(camc314): why do we have to destructure here?
90+ // In `./bindings.d.ts`, it doesn't indicate that we have to
91+ // (typed as `(filePath: string, bufferId: number, buffer: Uint8Array | undefined | null, ruleIds: number[])`).
92+ function lintFile ( [ filePath , bufferId , buffer , ruleIds ] ) {
93+ // If new buffer, add it to `buffers` array. Otherwise, get existing buffer from array.
94+ // Do this before checks below, to make sure buffer doesn't get garbage collected when not expected
95+ // if there's an error.
96+ // TODO: Is this enough to guarantee soundness?
97+ if ( buffer !== null ) {
98+ const { buffer : arrayBuffer , byteOffset } = buffer ;
99+ buffer . uint32 = new Uint32Array ( arrayBuffer , byteOffset ) ;
100+ buffer . float64 = new Float64Array ( arrayBuffer , byteOffset ) ;
101+
102+ while ( buffers . length <= bufferId ) {
103+ buffers . push ( null ) ;
81104 }
105+ buffers [ bufferId ] = buffer ;
106+ } else {
107+ buffer = buffers [ bufferId ] ;
82108 }
83109
84- // TODO(camc314): why do we have to destructure here?
85- // In `./bindings.d.ts`, it doesn't indicate that we have to
86- // (typed as `(filePath: string, bufferId: number, buffer: Uint8Array | undefined | null, ruleIds: number[])`).
87- lint ( [ filePath , bufferId , buffer , ruleIds ] ) {
88- // If new buffer, add it to `buffers` array. Otherwise, get existing buffer from array.
89- // Do this before checks below, to make sure buffer doesn't get garbage collected when not expected
90- // if there's an error.
91- // TODO: Is this enough to guarantee soundness?
92- if ( buffer !== null ) {
93- const { buffer : arrayBuffer , byteOffset } = buffer ;
94- buffer . uint32 = new Uint32Array ( arrayBuffer , byteOffset ) ;
95- buffer . float64 = new Float64Array ( arrayBuffer , byteOffset ) ;
96-
97- while ( buffers . length <= bufferId ) {
98- buffers . push ( null ) ;
99- }
100- buffers [ bufferId ] = buffer ;
101- } else {
102- buffer = buffers [ bufferId ] ;
103- }
104-
105- if ( typeof filePath !== 'string' || filePath . length === 0 ) {
106- throw new Error ( 'expected filePath to be a non-zero length string' ) ;
107- }
108- if ( ! Array . isArray ( ruleIds ) || ruleIds . length === 0 ) {
109- throw new Error ( 'Expected `ruleIds` to be a non-zero len array' ) ;
110- }
111-
112- // Get visitors for this file from all rules
113- const visitors = [ ] ;
114- for ( const { rule, ruleId } of this . pluginRegistry . getRules ( ruleIds ) ) {
115- visitors . push ( rule . create ( new Context ( ruleId , filePath ) ) ) ;
116- }
110+ if ( typeof filePath !== 'string' || filePath . length === 0 ) {
111+ throw new Error ( 'expected filePath to be a non-zero length string' ) ;
112+ }
113+ if ( ! Array . isArray ( ruleIds ) || ruleIds . length === 0 ) {
114+ throw new Error ( 'Expected `ruleIds` to be a non-zero len array' ) ;
115+ }
117116
118- const visitor = new Visitor (
119- visitors . length === 1 ? visitors [ 0 ] : combineVisitors ( visitors ) ,
120- ) ;
117+ // Get visitors for this file from all rules
118+ const visitors = ruleIds . map (
119+ ruleId => registeredRules [ ruleId ] . create ( new Context ( ruleId , filePath ) ) ,
120+ ) ;
121+ const visitor = new Visitor (
122+ visitors . length === 1 ? visitors [ 0 ] : combineVisitors ( visitors ) ,
123+ ) ;
121124
122- // Visit AST
123- const programPos = buffer . uint32 [ DATA_POINTER_POS_32 ] ,
124- sourceByteLen = buffer . uint32 [ SOURCE_LEN_POS_32 ] ;
125+ // Visit AST
126+ const programPos = buffer . uint32 [ DATA_POINTER_POS_32 ] ,
127+ sourceByteLen = buffer . uint32 [ SOURCE_LEN_POS_32 ] ;
125128
126- const sourceText = textDecoder . decode ( buffer . subarray ( 0 , sourceByteLen ) ) ;
127- const sourceIsAscii = sourceText . length === sourceByteLen ;
128- const ast = { buffer, sourceText, sourceByteLen, sourceIsAscii, nodes : new Map ( ) , token : TOKEN } ;
129+ const sourceText = textDecoder . decode ( buffer . subarray ( 0 , sourceByteLen ) ) ;
130+ const sourceIsAscii = sourceText . length === sourceByteLen ;
131+ const ast = { buffer, sourceText, sourceByteLen, sourceIsAscii, nodes : new Map ( ) , token : TOKEN } ;
129132
130- walkProgram ( programPos , ast , getVisitorsArr ( visitor ) ) ;
133+ walkProgram ( programPos , ast , getVisitorsArr ( visitor ) ) ;
131134
132- // Send diagnostics back to Rust
133- const ret = JSON . stringify ( diagnostics ) ;
134- diagnostics . length = 0 ;
135- return ret ;
136- }
135+ // Send diagnostics back to Rust
136+ const ret = JSON . stringify ( diagnostics ) ;
137+ diagnostics . length = 0 ;
138+ return ret ;
137139}
138140
141+ /**
142+ * Combine multiple visitor objects into a single visitor object.
143+ * @param {Array<Object> } visitors - Array of visitor objects
144+ * @returns {Object } - Combined visitor object
145+ */
139146function combineVisitors ( visitors ) {
140147 const combinedVisitor = { } ;
141148 for ( const visitor of visitors ) {
@@ -161,7 +168,7 @@ function combineVisitors(visitors) {
161168 * A `Context` is passed to each rule's `create` function.
162169 */
163170class Context {
164- // Rule ID. Index into `PluginRegistry`'s ` registeredRules` array.
171+ // Rule ID. Index into `registeredRules` array.
165172 #ruleId;
166173
167174 /**
@@ -192,15 +199,14 @@ class Context {
192199 }
193200}
194201
195- async function main ( ) {
196- const linter = new Linter ( ) ;
202+ // --------------------
203+ // Run linter
204+ // --------------------
197205
198- const success = await linter . run ( ) ;
199-
200- // Note: It's recommended to set `process.exitCode` instead of calling `process.exit()`.
201- // `process.exit()` kills the process immediately and `stdout` may not be flushed before process dies.
202- // https://nodejs.org/api/process.html#processexitcode
203- if ( ! success ) process . exitCode = 1 ;
204- }
206+ // Call Rust, passing `loadPlugin` and `lintFile` as callbacks
207+ const success = await lint ( loadPlugin , lintFile ) ;
205208
206- main ( ) ;
209+ // Note: It's recommended to set `process.exitCode` instead of calling `process.exit()`.
210+ // `process.exit()` kills the process immediately and `stdout` may not be flushed before process dies.
211+ // https://nodejs.org/api/process.html#processexitcode
212+ if ( ! success ) process . exitCode = 1 ;
0 commit comments