Skip to content

Commit 3678373

Browse files
committed
fix: refactor monaco validation to use acorn
1 parent fa93bb4 commit 3678373

File tree

4 files changed

+629
-78
lines changed

4 files changed

+629
-78
lines changed
Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api';
2+
3+
import { applyCustomScriptMarkers } from './monacoValidation';
4+
5+
// Mock Monaco types and create test helpers
6+
const createMockModel = (script: string): monacoType.editor.ITextModel => ({
7+
getValue: () => script,
8+
getLineContent: (lineNumber: number) => script.split('\n')[lineNumber - 1] || '',
9+
getLineCount: () => script.split('\n').length,
10+
getPositionAt: (offset: number) => {
11+
const lines = script.split('\n');
12+
let currentOffset = 0;
13+
for (let i = 0; i < lines.length; i++) {
14+
const lineLength = lines[i].length + 1; // +1 for newline
15+
if (currentOffset + lineLength > offset) {
16+
return { lineNumber: i + 1, column: offset - currentOffset + 1 };
17+
}
18+
currentOffset += lineLength;
19+
}
20+
return { lineNumber: lines.length, column: lines[lines.length - 1].length + 1 };
21+
},
22+
} as any);
23+
24+
const mockMonaco = {
25+
MarkerSeverity: { Error: 8 },
26+
editor: {
27+
setModelMarkers: jest.fn(),
28+
},
29+
} as any;
30+
31+
describe('Monaco K6 Validation', () => {
32+
beforeEach(() => {
33+
jest.clearAllMocks();
34+
});
35+
36+
describe('K6 Version Directives (Pragmas)', () => {
37+
describe('Should detect and flag', () => {
38+
test.each([
39+
// Basic version directives
40+
'"use k6 > 0.54";',
41+
"'use k6 >= v1.0.0';",
42+
'`use k6 < 2.0`;',
43+
'"use k6 <= v1.5.3";',
44+
"'use k6 == 1.2.0';",
45+
'"use k6 != v2.0.0-beta";',
46+
47+
// Version directives with extensions
48+
'"use k6 with k6/x/faker > 0.4.0";',
49+
"'use k6 with k6/x/sql >= 1.0.1';",
50+
'`use k6 with k6/x/kubernetes < 2.0.0`;',
51+
'"use k6 with k6/x/prometheus-remote-write != v1.0.0";',
52+
53+
// Different spacing
54+
'"use k6>0.54";',
55+
'"use k6 >= v1.0.0";',
56+
'"use k6 with k6/x/faker>0.4.0";',
57+
58+
// Version variations
59+
'"use k6 > 1";',
60+
'"use k6 >= 1.0";',
61+
'"use k6 < 1.2.3";',
62+
'"use k6 <= v1.2.3-alpha";',
63+
'"use k6 == 1.0.0+build.123";',
64+
])('standalone directive: %s', (directive) => {
65+
const script = `${directive}\nimport http from 'k6/http';`;
66+
const model = createMockModel(script);
67+
68+
applyCustomScriptMarkers(mockMonaco, model);
69+
70+
expect(mockMonaco.editor.setModelMarkers).toHaveBeenCalledWith(
71+
model,
72+
'k6-validation',
73+
expect.arrayContaining([
74+
expect.objectContaining({
75+
severity: 8, // Error
76+
message: 'Version directives cannot be used within scripts. Please remove any "use k6" statements.',
77+
code: 'k6-pragma-forbidden',
78+
startLineNumber: 1,
79+
})
80+
])
81+
);
82+
});
83+
84+
test('multiple directives in same script', () => {
85+
const script = `"use k6 > 0.54";
86+
"use k6 with k6/x/faker >= 1.0.0";
87+
import http from 'k6/http';`;
88+
89+
const model = createMockModel(script);
90+
applyCustomScriptMarkers(mockMonaco, model);
91+
92+
const [, , markers] = mockMonaco.editor.setModelMarkers.mock.calls[0];
93+
expect(markers).toHaveLength(2);
94+
expect(markers[0]).toMatchObject({
95+
message: 'Version directives cannot be used within scripts. Please remove any "use k6" statements.',
96+
startLineNumber: 1,
97+
});
98+
expect(markers[1]).toMatchObject({
99+
message: 'Version directives cannot be used within scripts. Please remove any "use k6" statements.',
100+
startLineNumber: 2,
101+
});
102+
});
103+
});
104+
105+
describe('Should NOT detect (context-aware)', () => {
106+
test.each([
107+
// Inside function calls
108+
'console.log("use k6 >= 1");',
109+
'alert("use k6 with k6/x/faker > 0.4.0");',
110+
'someFunction("use k6 < 2.0");',
111+
112+
// In variable assignments
113+
'const myVar = "use k6 >= 1";',
114+
'let directive = "use k6 with k6/x/sql >= 1.0.1";',
115+
'var version = "use k6 > 0.54";',
116+
117+
// In object properties
118+
'const config = { directive: "use k6 >= 1" };',
119+
'const obj = { "use k6 > 0.54": true };',
120+
121+
// In array literals
122+
'const directives = ["use k6 >= 1", "use k6 > 0.54"];',
123+
124+
// In return statements
125+
'return "use k6 >= 1";',
126+
127+
// In if conditions
128+
'if (script === "use k6 >= 1") {}',
129+
130+
// In template literals (non-standalone)
131+
'console.log(`Version: ${"use k6 >= 1"}`);',
132+
133+
// Comments (should be ignored completely)
134+
'// "use k6 >= 1"',
135+
'/* "use k6 with k6/x/faker > 0.4.0" */',
136+
137+
// Invalid syntax (not real directives)
138+
'"use k6";', // No operator
139+
'"use k7 >= 1";', // Wrong tool
140+
'"use k6 >= ";', // No version
141+
'"k6 >= 1";', // Missing "use"
142+
])('should ignore: %s', (code) => {
143+
const script = `${code}\nimport http from 'k6/http';`;
144+
const model = createMockModel(script);
145+
146+
applyCustomScriptMarkers(mockMonaco, model);
147+
148+
const [, , markers] = mockMonaco.editor.setModelMarkers.mock.calls[0];
149+
const pragmaMarkers = markers.filter((m: any) => m.code === 'k6-pragma-forbidden');
150+
expect(pragmaMarkers).toHaveLength(0);
151+
});
152+
});
153+
});
154+
155+
describe('K6 Extension Imports', () => {
156+
describe('Should detect and flag', () => {
157+
test.each([
158+
// Named imports
159+
'import { Faker } from "k6/x/faker";',
160+
'import { sql } from "k6/x/sql";',
161+
'import { check, group } from "k6/x/utils";',
162+
163+
// Default imports
164+
'import faker from "k6/x/faker";',
165+
'import sql from "k6/x/sql";',
166+
'import kubernetes from "k6/x/kubernetes";',
167+
168+
// Namespace imports
169+
'import * as faker from "k6/x/faker";',
170+
'import * as prometheus from "k6/x/prometheus-remote-write";',
171+
172+
// Mixed imports
173+
'import sql, { query } from "k6/x/sql";',
174+
'import faker, * as utils from "k6/x/faker";',
175+
176+
// Different quote styles
177+
"import faker from 'k6/x/faker';",
178+
// Note: Template literals are not valid in import statements in JavaScript
179+
180+
// Nested paths
181+
'import driver from "k6/x/sql/driver/postgres";',
182+
'import ramsql from "k6/x/sql/driver/ramsql";',
183+
'import auth from "k6/x/oauth/v2";',
184+
])('extension import: %s', (importStatement) => {
185+
const script = `${importStatement}\nimport http from 'k6/http';`;
186+
const model = createMockModel(script);
187+
188+
applyCustomScriptMarkers(mockMonaco, model);
189+
190+
expect(mockMonaco.editor.setModelMarkers).toHaveBeenCalledWith(
191+
model,
192+
'k6-validation',
193+
expect.arrayContaining([
194+
expect.objectContaining({
195+
severity: 8, // Error
196+
message: 'Script imports k6 extensions which are not allowed. Please remove imports from k6/x/ paths.',
197+
code: 'k6-extension-forbidden',
198+
startLineNumber: 1,
199+
})
200+
])
201+
);
202+
});
203+
204+
test('multiple extension imports', () => {
205+
const script = `import faker from "k6/x/faker";
206+
import sql from "k6/x/sql";
207+
import http from 'k6/http';`;
208+
209+
const model = createMockModel(script);
210+
applyCustomScriptMarkers(mockMonaco, model);
211+
212+
const [, , markers] = mockMonaco.editor.setModelMarkers.mock.calls[0];
213+
const extensionMarkers = markers.filter((m: any) => m.code === 'k6-extension-forbidden');
214+
expect(extensionMarkers).toHaveLength(2);
215+
});
216+
});
217+
218+
describe('Should NOT detect (standard k6 modules)', () => {
219+
test.each([
220+
// Standard k6 modules
221+
'import http from "k6/http";',
222+
'import { check, group } from "k6";',
223+
'import { Rate, Counter } from "k6/metrics";',
224+
'import { browser } from "k6/browser";',
225+
'import { crypto } from "k6/crypto";',
226+
'import { encoding } from "k6/encoding";',
227+
'import ws from "k6/ws";',
228+
229+
// External modules
230+
'import lodash from "lodash";',
231+
'import axios from "axios";',
232+
'import moment from "moment";',
233+
234+
// Relative imports
235+
'import utils from "./utils";',
236+
'import config from "../config";',
237+
'import helper from "../../helpers/test";',
238+
239+
// URL imports
240+
'import something from "https://example.com/lib.js";',
241+
242+
// Comments about k6/x (should be ignored)
243+
'// import faker from "k6/x/faker";',
244+
'/* import sql from "k6/x/sql"; */',
245+
246+
// Strings containing k6/x (should be ignored)
247+
'console.log("import faker from k6/x/faker");',
248+
'const note = "We used to use k6/x/sql";',
249+
])('should allow: %s', (importStatement) => {
250+
const script = `${importStatement}\nexport default function() {}`;
251+
const model = createMockModel(script);
252+
253+
applyCustomScriptMarkers(mockMonaco, model);
254+
255+
const [, , markers] = mockMonaco.editor.setModelMarkers.mock.calls[0];
256+
const extensionMarkers = markers.filter((m: any) => m.code === 'k6-extension-forbidden');
257+
expect(extensionMarkers).toHaveLength(0);
258+
});
259+
});
260+
});
261+
262+
describe('Combined scenarios', () => {
263+
test('script with both pragma and extension violations', () => {
264+
const script = `"use k6 > 0.54";
265+
import faker from "k6/x/faker";
266+
import http from 'k6/http';
267+
268+
export default function() {
269+
console.log("This should not trigger: use k6 >= 1");
270+
const variable = "use k6 with k6/x/sql >= 1.0.1";
271+
}`;
272+
273+
const model = createMockModel(script);
274+
applyCustomScriptMarkers(mockMonaco, model);
275+
276+
const [, , markers] = mockMonaco.editor.setModelMarkers.mock.calls[0];
277+
278+
expect(markers).toHaveLength(2);
279+
280+
const pragmaMarkers = markers.filter((m: any) => m.code === 'k6-pragma-forbidden');
281+
const extensionMarkers = markers.filter((m: any) => m.code === 'k6-extension-forbidden');
282+
283+
expect(pragmaMarkers).toHaveLength(1);
284+
expect(extensionMarkers).toHaveLength(1);
285+
286+
expect(pragmaMarkers[0]).toMatchObject({
287+
startLineNumber: 1,
288+
message: 'Version directives cannot be used within scripts. Please remove any "use k6" statements.',
289+
});
290+
291+
expect(extensionMarkers[0]).toMatchObject({
292+
startLineNumber: 2,
293+
message: 'Script imports k6 extensions which are not allowed. Please remove imports from k6/x/ paths.',
294+
});
295+
});
296+
297+
test('valid k6 script without violations', () => {
298+
const script = `import http from 'k6/http';
299+
import { check, group } from 'k6';
300+
import { Rate } from 'k6/metrics';
301+
302+
export default function() {
303+
const response = http.get('https://example.com');
304+
check(response, {
305+
'status is 200': (r) => r.status === 200,
306+
});
307+
}
308+
309+
export function handleSummary(data) {
310+
return {
311+
'summary.json': JSON.stringify(data),
312+
};
313+
}`;
314+
315+
const model = createMockModel(script);
316+
applyCustomScriptMarkers(mockMonaco, model);
317+
318+
const [, , markers] = mockMonaco.editor.setModelMarkers.mock.calls[0];
319+
expect(markers).toHaveLength(0);
320+
});
321+
});
322+
323+
describe('Edge cases', () => {
324+
test('empty script', () => {
325+
const model = createMockModel('');
326+
applyCustomScriptMarkers(mockMonaco, model);
327+
328+
const [, , markers] = mockMonaco.editor.setModelMarkers.mock.calls[0];
329+
expect(markers).toHaveLength(0);
330+
});
331+
332+
test('script with syntax errors (should not crash)', () => {
333+
const script = 'import { unclosed from "k6/x/faker"'; // Syntax error
334+
const model = createMockModel(script);
335+
336+
// Should not throw
337+
expect(() => {
338+
applyCustomScriptMarkers(mockMonaco, model);
339+
}).not.toThrow();
340+
341+
// Should return empty markers for invalid syntax
342+
const [, , markers] = mockMonaco.editor.setModelMarkers.mock.calls[0];
343+
expect(markers).toHaveLength(0);
344+
});
345+
346+
test('template literals as standalone expressions', () => {
347+
const script = '`use k6 >= v1.0.0`;\nimport http from "k6/http";';
348+
const model = createMockModel(script);
349+
350+
applyCustomScriptMarkers(mockMonaco, model);
351+
352+
const [, , markers] = mockMonaco.editor.setModelMarkers.mock.calls[0];
353+
const pragmaMarkers = markers.filter((m: any) => m.code === 'k6-pragma-forbidden');
354+
expect(pragmaMarkers).toHaveLength(1);
355+
});
356+
357+
test('nested template literals (should be ignored)', () => {
358+
const script = 'console.log(`Version: ${`use k6 >= 1`}`);';
359+
const model = createMockModel(script);
360+
361+
applyCustomScriptMarkers(mockMonaco, model);
362+
363+
const [, , markers] = mockMonaco.editor.setModelMarkers.mock.calls[0];
364+
const pragmaMarkers = markers.filter((m: any) => m.code === 'k6-pragma-forbidden');
365+
expect(pragmaMarkers).toHaveLength(0);
366+
});
367+
});
368+
});

0 commit comments

Comments
 (0)