norn-cli 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/AGENTS.md +9 -1
  2. package/CHANGELOG.md +23 -0
  3. package/dist/cli.js +246 -80
  4. package/package.json +1 -1
  5. package/out/assertionRunner.js +0 -537
  6. package/out/chatParticipant.js +0 -722
  7. package/out/cli/colors.js +0 -129
  8. package/out/cli/formatters/assertion.js +0 -75
  9. package/out/cli/formatters/index.js +0 -23
  10. package/out/cli/formatters/response.js +0 -106
  11. package/out/cli/formatters/summary.js +0 -187
  12. package/out/cli/redaction.js +0 -237
  13. package/out/cli/reporters/html.js +0 -634
  14. package/out/cli/reporters/index.js +0 -22
  15. package/out/cli/reporters/junit.js +0 -211
  16. package/out/cli.js +0 -989
  17. package/out/codeLensProvider.js +0 -248
  18. package/out/compareContentProvider.js +0 -85
  19. package/out/completionProvider.js +0 -2404
  20. package/out/contractDecorationProvider.js +0 -243
  21. package/out/coverageCalculator.js +0 -837
  22. package/out/coveragePanel.js +0 -545
  23. package/out/diagnosticProvider.js +0 -1113
  24. package/out/environmentProvider.js +0 -442
  25. package/out/extension.js +0 -1114
  26. package/out/httpClient.js +0 -269
  27. package/out/jsonFileReader.js +0 -320
  28. package/out/nornPrompt.js +0 -580
  29. package/out/nornapiParser.js +0 -326
  30. package/out/parser.js +0 -725
  31. package/out/responsePanel.js +0 -4674
  32. package/out/schemaGenerator.js +0 -393
  33. package/out/scriptRunner.js +0 -419
  34. package/out/sequenceRunner.js +0 -3046
  35. package/out/swaggerBodyIntellisenseCache.js +0 -147
  36. package/out/swaggerParser.js +0 -419
  37. package/out/test/coverageCalculator.test.js +0 -100
  38. package/out/test/extension.test.js +0 -48
  39. package/out/testProvider.js +0 -658
  40. package/out/validationCache.js +0 -245
@@ -1,1113 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.DiagnosticProvider = void 0;
37
- const vscode = __importStar(require("vscode"));
38
- const path = __importStar(require("path"));
39
- const fs = __importStar(require("fs"));
40
- const parser_1 = require("./parser");
41
- const sequenceRunner_1 = require("./sequenceRunner");
42
- const environmentProvider_1 = require("./environmentProvider");
43
- const nornapiParser_1 = require("./nornapiParser");
44
- /**
45
- * Extracts variables defined within a specific sequence, including:
46
- * - var statements
47
- * - var x = run Sequence (capture from returned sequences)
48
- * - Response captures (var x = $1.body)
49
- * - Sequence parameters (from the sequence declaration)
50
- */
51
- function getSequenceLocalVariables(sequenceContent, parameters) {
52
- const vars = new Set();
53
- // Add sequence parameters as local variables
54
- if (parameters) {
55
- for (const param of parameters) {
56
- vars.add(param.name);
57
- }
58
- }
59
- const lines = sequenceContent.split('\n');
60
- for (const line of lines) {
61
- const trimmed = line.trim();
62
- // Match any var assignment
63
- const varMatch = trimmed.match(/^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=/);
64
- if (varMatch) {
65
- vars.add(varMatch[1]);
66
- }
67
- }
68
- return vars;
69
- }
70
- /**
71
- * Check if a line is a comment (starts with #)
72
- */
73
- function isCommentLine(line) {
74
- return line.trim().startsWith('#');
75
- }
76
- /**
77
- * Strip inline comment from a line.
78
- * Finds # that is not inside quotes and removes everything after it.
79
- */
80
- function stripInlineComment(line) {
81
- let inSingleQuote = false;
82
- let inDoubleQuote = false;
83
- for (let i = 0; i < line.length; i++) {
84
- const char = line[i];
85
- const prevChar = i > 0 ? line[i - 1] : '';
86
- // Skip escaped quotes
87
- if (prevChar === '\\') {
88
- continue;
89
- }
90
- if (char === '"' && !inSingleQuote) {
91
- inDoubleQuote = !inDoubleQuote;
92
- }
93
- else if (char === "'" && !inDoubleQuote) {
94
- inSingleQuote = !inSingleQuote;
95
- }
96
- else if (char === '#' && !inSingleQuote && !inDoubleQuote) {
97
- // Found an unquoted #, strip from here
98
- return line.substring(0, i).trimEnd();
99
- }
100
- }
101
- return line;
102
- }
103
- function isHttpRequestStart(line) {
104
- return /^(?:var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)?(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i.test(line.trim());
105
- }
106
- function isRequestBodyBoundary(line) {
107
- const trimmed = line.trim();
108
- if (!trimmed) {
109
- return false;
110
- }
111
- return isHttpRequestStart(trimmed) ||
112
- /^\[/.test(trimmed) ||
113
- /^(?:test\s+)?sequence\s+/i.test(trimmed) ||
114
- /^end\s+sequence$/i.test(trimmed) ||
115
- /^(?:var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)?run\s+/i.test(trimmed) ||
116
- /^assert\s+/i.test(trimmed) ||
117
- /^print\s+/i.test(trimmed) ||
118
- /^if\s+/i.test(trimmed) ||
119
- /^foreach\s+/i.test(trimmed) ||
120
- /^return\s+/i.test(trimmed);
121
- }
122
- function countChar(value, char) {
123
- let count = 0;
124
- for (let i = 0; i < value.length; i++) {
125
- if (value[i] === char) {
126
- count++;
127
- }
128
- }
129
- return count;
130
- }
131
- function looksLikeJsonPropertyLine(line) {
132
- return /^"[^"\\]*(?:\\.[^"\\]*)*"\s*:\s*.+$/.test(line.trim());
133
- }
134
- function getHeaderValueCaseInsensitive(headers, headerName) {
135
- const target = headerName.toLowerCase();
136
- for (const [name, value] of Object.entries(headers)) {
137
- if (name.toLowerCase() === target) {
138
- return value;
139
- }
140
- }
141
- return undefined;
142
- }
143
- /**
144
- * Tokenize print statement content into individual tokens.
145
- * Respects quoted strings and splits on operators/whitespace.
146
- */
147
- function tokenizePrintContent(content) {
148
- const tokens = [];
149
- let current = '';
150
- let currentStart = 0;
151
- let inString = false;
152
- let stringChar = '';
153
- let i = 0;
154
- const pushToken = () => {
155
- if (current.trim()) {
156
- tokens.push({ text: current.trim(), start: currentStart + (current.length - current.trimStart().length) });
157
- }
158
- current = '';
159
- };
160
- while (i < content.length) {
161
- const char = content[i];
162
- if (inString) {
163
- current += char;
164
- if (char === stringChar && content[i - 1] !== '\\') {
165
- inString = false;
166
- pushToken();
167
- }
168
- i++;
169
- continue;
170
- }
171
- // Start of string
172
- if (char === '"' || char === "'") {
173
- pushToken();
174
- inString = true;
175
- stringChar = char;
176
- current = char;
177
- currentStart = i;
178
- i++;
179
- continue;
180
- }
181
- // Operators and whitespace are delimiters
182
- if (/[\s+\-*/|&<>=!]/.test(char)) {
183
- pushToken();
184
- // If it's an operator, add it as its own token
185
- if (/[+\-*/|&<>=!]/.test(char)) {
186
- tokens.push({ text: char, start: i });
187
- }
188
- currentStart = i + 1;
189
- i++;
190
- continue;
191
- }
192
- // Regular character
193
- if (current === '') {
194
- currentStart = i;
195
- }
196
- current += char;
197
- i++;
198
- }
199
- // Push any remaining token
200
- pushToken();
201
- return tokens;
202
- }
203
- class DiagnosticProvider {
204
- diagnosticCollection;
205
- constructor() {
206
- this.diagnosticCollection = vscode.languages.createDiagnosticCollection('norn');
207
- }
208
- updateDiagnostics(document) {
209
- if (document.languageId === 'nornenv') {
210
- this.updateNornenvDiagnostics(document);
211
- return;
212
- }
213
- if (document.languageId === 'nornapi') {
214
- this.updateNornapiDiagnostics(document);
215
- return;
216
- }
217
- if (document.languageId !== 'norn') {
218
- return;
219
- }
220
- const diagnostics = [];
221
- const text = document.getText();
222
- // Extract file-level variables (outside sequences) + environment
223
- const fileLevelVariables = (0, parser_1.extractFileLevelVariables)(text);
224
- const envVariables = (0, environmentProvider_1.getEnvironmentVariables)(document.uri.fsPath);
225
- const globalVariables = { ...envVariables, ...fileLevelVariables };
226
- // Extract sequences for scope-aware checking
227
- const sequences = (0, sequenceRunner_1.extractSequences)(text);
228
- const lines = text.split('\n');
229
- // Check for duplicate sequence declarations
230
- const declaredSequences = new Map(); // seqName -> first declaration line
231
- for (const seq of sequences) {
232
- if (declaredSequences.has(seq.name)) {
233
- const firstLine = declaredSequences.get(seq.name);
234
- const line = lines[seq.startLine];
235
- // Find the sequence name position in the line
236
- const nameStart = line.indexOf(seq.name);
237
- const range = new vscode.Range(new vscode.Position(seq.startLine, nameStart), new vscode.Position(seq.startLine, nameStart + seq.name.length));
238
- const diagnostic = new vscode.Diagnostic(range, `Duplicate sequence: '${seq.name}' is already declared on line ${firstLine + 1}.`, vscode.DiagnosticSeverity.Error);
239
- diagnostic.source = 'Norn';
240
- diagnostic.code = 'duplicate-sequence';
241
- diagnostics.push(diagnostic);
242
- }
243
- else {
244
- declaredSequences.set(seq.name, seq.startLine);
245
- }
246
- }
247
- // Check for duplicate variable declarations at file level (outside sequences)
248
- const varDeclRegex = /^\s*var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=/;
249
- const declaredVars = new Map(); // varName -> first declaration line
250
- for (let i = 0; i < lines.length; i++) {
251
- const line = lines[i];
252
- // Skip comment lines
253
- if (isCommentLine(line)) {
254
- continue;
255
- }
256
- // Check if this line is inside a sequence (skip sequence-local vars)
257
- const isInsideSequence = sequences.some(seq => i > seq.startLine && i < seq.endLine);
258
- if (isInsideSequence) {
259
- continue;
260
- }
261
- const lineWithoutComment = stripInlineComment(line);
262
- const varMatch = lineWithoutComment.match(varDeclRegex);
263
- if (varMatch) {
264
- const varName = varMatch[1];
265
- if (declaredVars.has(varName)) {
266
- const firstLine = declaredVars.get(varName);
267
- const startCol = line.indexOf(varName);
268
- const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, startCol + varName.length));
269
- const diagnostic = new vscode.Diagnostic(range, `Duplicate variable declaration: '${varName}' is already declared on line ${firstLine + 1}.`, vscode.DiagnosticSeverity.Error);
270
- diagnostic.source = 'Norn';
271
- diagnostic.code = 'duplicate-variable';
272
- diagnostics.push(diagnostic);
273
- }
274
- else {
275
- declaredVars.set(varName, i);
276
- }
277
- }
278
- }
279
- // Check for invalid named request declarations (names with spaces) and duplicates
280
- const invalidNameRegex = /^\s*\[(?:Name:\s*)?(.+)\]\s*$/;
281
- const namedRequestRegex = /^\s*\[(?:Name:\s*)?([a-zA-Z_][a-zA-Z0-9_-]*)\]/;
282
- const validNameRegex = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
283
- const declaredNamedRequests = new Map(); // name -> first declaration line
284
- for (let i = 0; i < lines.length; i++) {
285
- const line = lines[i];
286
- // Skip comment lines
287
- if (isCommentLine(line)) {
288
- continue;
289
- }
290
- // Strip inline comments before checking
291
- const lineWithoutComment = stripInlineComment(line);
292
- // Check for valid named request and track for duplicates
293
- const namedMatch = lineWithoutComment.match(namedRequestRegex);
294
- if (namedMatch) {
295
- const name = namedMatch[1];
296
- if (declaredNamedRequests.has(name)) {
297
- const firstLine = declaredNamedRequests.get(name);
298
- const startCol = line.indexOf('[');
299
- const endCol = line.indexOf(']') + 1;
300
- const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, endCol));
301
- const diagnostic = new vscode.Diagnostic(range, `Duplicate named request: '${name}' is already declared on line ${firstLine + 1}.`, vscode.DiagnosticSeverity.Error);
302
- diagnostic.source = 'Norn';
303
- diagnostic.code = 'duplicate-named-request';
304
- diagnostics.push(diagnostic);
305
- }
306
- else {
307
- declaredNamedRequests.set(name, i);
308
- }
309
- }
310
- // Check for invalid names (with spaces etc)
311
- const match = lineWithoutComment.match(invalidNameRegex);
312
- if (match) {
313
- const name = match[1].trim();
314
- if (!validNameRegex.test(name)) {
315
- const startCol = line.indexOf('[');
316
- const endCol = line.indexOf(']') + 1;
317
- const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, endCol));
318
- const diagnostic = new vscode.Diagnostic(range, `Invalid request name: '${name}'. Names cannot contain spaces and must start with a letter or underscore.`, vscode.DiagnosticSeverity.Error);
319
- diagnostic.source = 'Norn';
320
- diagnostic.code = 'invalid-request-name';
321
- diagnostics.push(diagnostic);
322
- }
323
- }
324
- }
325
- // Check for import statement errors
326
- const imports = (0, parser_1.extractImports)(text);
327
- const documentDir = path.dirname(document.uri.fsPath);
328
- // Track imported names with source info for duplicate detection
329
- // Maps name (lowercase) -> { sourcePath, lineNumber }
330
- const importedRequests = new Map();
331
- const importedSequences = new Map();
332
- // Track imported API definitions (endpoints and header groups from .nornapi files)
333
- // Maps name -> { sourcePath, lineNumber } to detect duplicates
334
- const importedEndpoints = new Map();
335
- const importedHeaderGroups = new Map();
336
- for (const imp of imports) {
337
- const absolutePath = path.resolve(documentDir, imp.path);
338
- // Check if file exists
339
- if (!fs.existsSync(absolutePath)) {
340
- const line = lines[imp.lineNumber];
341
- const startCol = line.indexOf(imp.path);
342
- const range = new vscode.Range(new vscode.Position(imp.lineNumber, startCol), new vscode.Position(imp.lineNumber, startCol + imp.path.length));
343
- const diagnostic = new vscode.Diagnostic(range, `Import file not found: '${imp.path}'`, vscode.DiagnosticSeverity.Error);
344
- diagnostic.source = 'Norn';
345
- diagnostic.code = 'import-not-found';
346
- diagnostics.push(diagnostic);
347
- }
348
- else {
349
- // Read the imported file and extract its named requests/sequences
350
- try {
351
- const importedContent = fs.readFileSync(absolutePath, 'utf-8');
352
- // Check if this is a .nornapi file
353
- if (imp.path.endsWith('.nornapi')) {
354
- const apiDef = (0, nornapiParser_1.parseNornApiFile)(importedContent);
355
- for (const endpoint of apiDef.endpoints) {
356
- const existing = importedEndpoints.get(endpoint.name);
357
- if (existing) {
358
- // Duplicate endpoint found
359
- const line = lines[imp.lineNumber];
360
- const startCol = line.indexOf(imp.path);
361
- const range = new vscode.Range(new vscode.Position(imp.lineNumber, startCol), new vscode.Position(imp.lineNumber, startCol + imp.path.length));
362
- const diagnostic = new vscode.Diagnostic(range, `Duplicate endpoint '${endpoint.name}': already defined in '${existing.sourcePath}'`, vscode.DiagnosticSeverity.Error);
363
- diagnostic.source = 'Norn';
364
- diagnostic.code = 'duplicate-endpoint';
365
- diagnostics.push(diagnostic);
366
- }
367
- else {
368
- importedEndpoints.set(endpoint.name, { sourcePath: imp.path, lineNumber: imp.lineNumber });
369
- }
370
- }
371
- for (const group of apiDef.headerGroups) {
372
- const existing = importedHeaderGroups.get(group.name);
373
- if (existing) {
374
- // Duplicate header group found
375
- const line = lines[imp.lineNumber];
376
- const startCol = line.indexOf(imp.path);
377
- const range = new vscode.Range(new vscode.Position(imp.lineNumber, startCol), new vscode.Position(imp.lineNumber, startCol + imp.path.length));
378
- const diagnostic = new vscode.Diagnostic(range, `Duplicate header group '${group.name}': already defined in '${existing.sourcePath}'`, vscode.DiagnosticSeverity.Error);
379
- diagnostic.source = 'Norn';
380
- diagnostic.code = 'duplicate-header-group';
381
- diagnostics.push(diagnostic);
382
- }
383
- else {
384
- importedHeaderGroups.set(group.name, {
385
- sourcePath: imp.path,
386
- lineNumber: imp.lineNumber,
387
- headerGroup: group
388
- });
389
- }
390
- }
391
- }
392
- else {
393
- // Regular .norn file
394
- const fileRequests = (0, parser_1.extractNamedRequests)(importedContent);
395
- const fileSequences = (0, sequenceRunner_1.extractSequences)(importedContent);
396
- // Check for duplicate named requests
397
- for (const req of fileRequests) {
398
- const lowerName = req.name.toLowerCase();
399
- const existing = importedRequests.get(lowerName);
400
- if (existing) {
401
- const line = lines[imp.lineNumber];
402
- const startCol = line.indexOf(imp.path);
403
- const range = new vscode.Range(new vscode.Position(imp.lineNumber, startCol), new vscode.Position(imp.lineNumber, startCol + imp.path.length));
404
- const diagnostic = new vscode.Diagnostic(range, `Duplicate named request '${req.name}': already defined in '${existing.sourcePath}'`, vscode.DiagnosticSeverity.Error);
405
- diagnostic.source = 'Norn';
406
- diagnostic.code = 'duplicate-named-request-import';
407
- diagnostics.push(diagnostic);
408
- }
409
- else {
410
- importedRequests.set(lowerName, { sourcePath: imp.path, lineNumber: imp.lineNumber });
411
- }
412
- }
413
- // Check for duplicate sequences
414
- for (const seq of fileSequences) {
415
- const lowerName = seq.name.toLowerCase();
416
- const existing = importedSequences.get(lowerName);
417
- if (existing) {
418
- const line = lines[imp.lineNumber];
419
- const startCol = line.indexOf(imp.path);
420
- const range = new vscode.Range(new vscode.Position(imp.lineNumber, startCol), new vscode.Position(imp.lineNumber, startCol + imp.path.length));
421
- const diagnostic = new vscode.Diagnostic(range, `Duplicate sequence '${seq.name}': already defined in '${existing.sourcePath}'`, vscode.DiagnosticSeverity.Error);
422
- diagnostic.source = 'Norn';
423
- diagnostic.code = 'duplicate-sequence-import';
424
- diagnostics.push(diagnostic);
425
- }
426
- else {
427
- importedSequences.set(lowerName, { sourcePath: imp.path, lineNumber: imp.lineNumber });
428
- }
429
- }
430
- }
431
- }
432
- catch {
433
- // Ignore read errors - file exists but couldn't be read
434
- }
435
- }
436
- }
437
- // Check for misplaced tags/annotations.
438
- // Any @decorator line must be directly associated with a following sequence declaration.
439
- const tagPattern = /^\s*@([a-zA-Z_][a-zA-Z0-9_-]*)(?:\(([^)]*)\))?/;
440
- const sequencePattern = /^\s*(?:test\s+)?sequence\s+[a-zA-Z_][a-zA-Z0-9_-]*/;
441
- for (let i = 0; i < lines.length; i++) {
442
- const line = lines[i];
443
- // Skip comment lines
444
- if (isCommentLine(line)) {
445
- continue;
446
- }
447
- // Check if this line contains a tag/annotation decorator
448
- if (tagPattern.test(line)) {
449
- // Look ahead to find what this decorator applies to
450
- let foundSequence = false;
451
- let j = i + 1;
452
- // Skip empty lines and additional decorator lines
453
- while (j < lines.length) {
454
- const nextLine = lines[j].trim();
455
- if (!nextLine) {
456
- j++;
457
- continue;
458
- }
459
- if (tagPattern.test(nextLine)) {
460
- j++;
461
- continue;
462
- }
463
- if (sequencePattern.test(nextLine)) {
464
- foundSequence = true;
465
- }
466
- break;
467
- }
468
- const tagMatch = line.match(/@([a-zA-Z_][a-zA-Z0-9_-]*)(?:\([^)]+\))?/);
469
- if (tagMatch) {
470
- const startCol = line.indexOf('@');
471
- const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, startCol + tagMatch[0].length));
472
- if (!foundSequence) {
473
- const annotationName = tagMatch[1];
474
- const isDataAnnotation = annotationName === 'data' || annotationName === 'theory';
475
- const message = isDataAnnotation
476
- ? `@${annotationName} must be placed immediately before a sequence declaration.`
477
- : `Tags must be placed immediately before a sequence declaration.`;
478
- const diagnostic = new vscode.Diagnostic(range, message, vscode.DiagnosticSeverity.Error);
479
- diagnostic.source = 'Norn';
480
- diagnostic.code = 'misplaced-tag';
481
- diagnostics.push(diagnostic);
482
- }
483
- }
484
- }
485
- }
486
- // Check for test sequences with required parameters but no @data/@theory
487
- for (const seq of sequences) {
488
- if (seq.isTest) {
489
- const requiredParams = seq.parameters.filter(p => p.defaultValue === undefined);
490
- if (requiredParams.length > 0 && !seq.theoryData) {
491
- // Find the line with the 'test sequence' declaration
492
- let declarationLine = seq.startLine;
493
- for (let i = seq.startLine; i <= seq.endLine; i++) {
494
- if (/^\s*test\s+sequence\s+/.test(lines[i])) {
495
- declarationLine = i;
496
- break;
497
- }
498
- }
499
- const line = lines[declarationLine];
500
- const startCol = line.indexOf('test');
501
- const endCol = line.length;
502
- const range = new vscode.Range(new vscode.Position(declarationLine, startCol), new vscode.Position(declarationLine, endCol));
503
- const paramNames = requiredParams.map(p => p.name).join(', ');
504
- const diagnostic = new vscode.Diagnostic(range, `Test sequence '${seq.name}' has required parameters (${paramNames}) but no @data or @theory annotation. Add @data(...) or @theory("file.json") to provide test values.`, vscode.DiagnosticSeverity.Error);
505
- diagnostic.source = 'Norn';
506
- diagnostic.code = 'test-missing-data';
507
- diagnostics.push(diagnostic);
508
- }
509
- }
510
- }
511
- // Check for undefined named requests/sequences in 'run' commands
512
- const namedRequests = (0, parser_1.extractNamedRequests)(text);
513
- const namedRequestNames = new Set(namedRequests.map(r => r.name.toLowerCase()));
514
- const sequenceNames = new Set(sequences.map(s => s.name.toLowerCase()));
515
- // Build a map of sequence name -> required parameter count
516
- const sequenceParamInfo = new Map();
517
- for (const seq of sequences) {
518
- const requiredParams = seq.parameters.filter(p => p.defaultValue === undefined).length;
519
- sequenceParamInfo.set(seq.name.toLowerCase(), { required: requiredParams, total: seq.parameters.length });
520
- }
521
- // Updated regex to capture optional arguments: run Name or run Name(args)
522
- const runCommandRegex = /^\s*(?:var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)?run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\(([^)]*)\))?\s*$/;
523
- for (let i = 0; i < lines.length; i++) {
524
- const line = lines[i];
525
- // Skip comment lines
526
- if (isCommentLine(line)) {
527
- continue;
528
- }
529
- const match = line.match(runCommandRegex);
530
- if (match) {
531
- const requestName = match[1];
532
- const argsStr = match[2]; // undefined if no parens, empty string if (), or "arg1, arg2"
533
- const lowerName = requestName.toLowerCase();
534
- // Skip if it's a script command
535
- if (['bash', 'js', 'powershell'].includes(lowerName)) {
536
- continue;
537
- }
538
- // Check if it's a local named request, local sequence, or imported
539
- const isLocalRequest = namedRequestNames.has(lowerName);
540
- const isLocalSequence = sequenceNames.has(lowerName);
541
- const isImportedRequest = importedRequests.has(lowerName);
542
- const isImportedSequence = importedSequences.has(lowerName);
543
- if (!isLocalRequest && !isLocalSequence && !isImportedRequest && !isImportedSequence) {
544
- const startCol = line.indexOf(requestName);
545
- const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, startCol + requestName.length));
546
- const diagnostic = new vscode.Diagnostic(range, `Undefined request or sequence: '${requestName}'`, vscode.DiagnosticSeverity.Error);
547
- diagnostic.source = 'Norn';
548
- diagnostic.code = 'undefined-request';
549
- diagnostics.push(diagnostic);
550
- }
551
- else if (isLocalSequence) {
552
- // Check if sequence requires parameters but none were provided
553
- const paramInfo = sequenceParamInfo.get(lowerName);
554
- if (paramInfo && paramInfo.required > 0) {
555
- // Count provided arguments
556
- let providedCount = 0;
557
- if (argsStr !== undefined && argsStr.trim() !== '') {
558
- // Split by comma, but respect quoted strings
559
- const args = argsStr.split(',').map(a => a.trim()).filter(a => a !== '');
560
- providedCount = args.length;
561
- }
562
- if (providedCount < paramInfo.required) {
563
- const startCol = line.indexOf(requestName);
564
- const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, startCol + requestName.length));
565
- const seq = sequences.find(s => s.name.toLowerCase() === lowerName);
566
- const requiredParamNames = seq ?
567
- seq.parameters.filter(p => p.defaultValue === undefined).map(p => p.name).join(', ') :
568
- '';
569
- const diagnostic = new vscode.Diagnostic(range, `Sequence '${requestName}' requires ${paramInfo.required} parameter(s): ${requiredParamNames}`, vscode.DiagnosticSeverity.Error);
570
- diagnostic.source = 'Norn';
571
- diagnostic.code = 'missing-sequence-parameters';
572
- diagnostics.push(diagnostic);
573
- }
574
- }
575
- }
576
- }
577
- }
578
- // Check for undefined API endpoints and header groups
579
- // Match patterns like: GET EndpointName("arg") HeaderGroup
580
- const apiRequestRegex = /^\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([A-Z][a-zA-Z0-9_]*)(?:\([^)]*\))?(?:\s+([A-Z][a-zA-Z0-9_]*))?/;
581
- // Match standalone header group on its own line (after an API request)
582
- const standaloneHeaderGroupRegex = /^\s+([A-Z][a-zA-Z0-9_]*)\s*$/;
583
- for (let i = 0; i < lines.length; i++) {
584
- const line = lines[i];
585
- // Skip comment lines
586
- if (isCommentLine(line)) {
587
- continue;
588
- }
589
- // Strip inline comments before checking API request syntax
590
- const lineWithoutComment = stripInlineComment(line);
591
- // Check API request syntax
592
- const apiMatch = lineWithoutComment.match(apiRequestRegex);
593
- if (apiMatch) {
594
- const endpointName = apiMatch[2];
595
- const headerGroupName = apiMatch[3];
596
- // Check if endpoint exists
597
- if (!importedEndpoints.has(endpointName)) {
598
- const startCol = line.indexOf(endpointName);
599
- const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, startCol + endpointName.length));
600
- const diagnostic = new vscode.Diagnostic(range, `Undefined endpoint: '${endpointName}'. Import a .nornapi file that defines this endpoint.`, vscode.DiagnosticSeverity.Error);
601
- diagnostic.source = 'Norn';
602
- diagnostic.code = 'undefined-endpoint';
603
- diagnostics.push(diagnostic);
604
- }
605
- // Check if header group exists (if specified)
606
- if (headerGroupName && !importedHeaderGroups.has(headerGroupName)) {
607
- const startCol = line.lastIndexOf(headerGroupName);
608
- const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, startCol + headerGroupName.length));
609
- const diagnostic = new vscode.Diagnostic(range, `Undefined header group: '${headerGroupName}'. Import a .nornapi file that defines this header group.`, vscode.DiagnosticSeverity.Error);
610
- diagnostic.source = 'Norn';
611
- diagnostic.code = 'undefined-header-group';
612
- diagnostics.push(diagnostic);
613
- }
614
- }
615
- // Check standalone header group lines (indented, capitalized identifier only)
616
- const standaloneMatch = lineWithoutComment.match(standaloneHeaderGroupRegex);
617
- if (standaloneMatch) {
618
- const headerGroupName = standaloneMatch[1];
619
- // Only flag as undefined if we have some .nornapi imports (otherwise it might just be a typo or URL)
620
- if (importedEndpoints.size > 0 || importedHeaderGroups.size > 0) {
621
- if (!importedHeaderGroups.has(headerGroupName)) {
622
- const startCol = line.indexOf(headerGroupName);
623
- const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, startCol + headerGroupName.length));
624
- const diagnostic = new vscode.Diagnostic(range, `Undefined header group: '${headerGroupName}'. Import a .nornapi file that defines this header group.`, vscode.DiagnosticSeverity.Error);
625
- diagnostic.source = 'Norn';
626
- diagnostic.code = 'undefined-header-group';
627
- diagnostics.push(diagnostic);
628
- }
629
- }
630
- }
631
- }
632
- // Check for JSON-like request body properties without surrounding { } object
633
- const bodyMethods = new Set(['POST', 'PUT', 'PATCH']);
634
- const requestStartRegex = /^\s*(?:var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)?(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i;
635
- const inlineHeaderRegex = /^([A-Za-z][A-Za-z0-9\-]*)\s*:\s*(.+)$/;
636
- const formContentTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];
637
- for (let i = 0; i < lines.length; i++) {
638
- const line = lines[i];
639
- if (isCommentLine(line)) {
640
- continue;
641
- }
642
- const lineWithoutComment = stripInlineComment(line).trim();
643
- const requestMatch = lineWithoutComment.match(requestStartRegex);
644
- if (!requestMatch) {
645
- continue;
646
- }
647
- const method = requestMatch[1].toUpperCase();
648
- if (!bodyMethods.has(method)) {
649
- continue;
650
- }
651
- let requestContentType;
652
- const afterMethod = lineWithoutComment.replace(requestStartRegex, '').trim();
653
- const methodLineTokens = afterMethod.split(/\s+/).filter(t => t.length > 0);
654
- for (const token of methodLineTokens) {
655
- const cleanToken = token.replace(/[,)]+$/g, '');
656
- const group = importedHeaderGroups.get(cleanToken)?.headerGroup;
657
- if (group) {
658
- const headerValue = getHeaderValueCaseInsensitive(group.headers, 'Content-Type');
659
- if (headerValue) {
660
- requestContentType = headerValue.toLowerCase();
661
- }
662
- }
663
- }
664
- let objectDepth = 0;
665
- let hasReported = false;
666
- let reachedBody = false;
667
- for (let j = i + 1; j < lines.length; j++) {
668
- const bodyLine = lines[j];
669
- if (isCommentLine(bodyLine)) {
670
- continue;
671
- }
672
- const bodyLineWithoutComment = stripInlineComment(bodyLine);
673
- const trimmedBodyLine = bodyLineWithoutComment.trim();
674
- if (!trimmedBodyLine) {
675
- continue;
676
- }
677
- if (isRequestBodyBoundary(trimmedBodyLine)) {
678
- break;
679
- }
680
- if (!reachedBody) {
681
- const headerMatch = trimmedBodyLine.match(inlineHeaderRegex);
682
- if (headerMatch) {
683
- if (headerMatch[1].toLowerCase() === 'content-type') {
684
- requestContentType = headerMatch[2].trim().toLowerCase();
685
- }
686
- continue;
687
- }
688
- const headerGroup = importedHeaderGroups.get(trimmedBodyLine)?.headerGroup;
689
- if (headerGroup) {
690
- const headerValue = getHeaderValueCaseInsensitive(headerGroup.headers, 'Content-Type');
691
- if (headerValue) {
692
- requestContentType = headerValue.toLowerCase();
693
- }
694
- continue;
695
- }
696
- }
697
- reachedBody = true;
698
- const isFormBody = requestContentType
699
- ? formContentTypes.some(ct => requestContentType.includes(ct))
700
- : false;
701
- if (isFormBody) {
702
- continue;
703
- }
704
- const startsJsonObject = trimmedBodyLine.startsWith('{');
705
- if (!hasReported && looksLikeJsonPropertyLine(trimmedBodyLine) && objectDepth === 0 && !startsJsonObject) {
706
- const startCol = bodyLine.indexOf('"');
707
- const range = new vscode.Range(new vscode.Position(j, startCol >= 0 ? startCol : 0), new vscode.Position(j, bodyLine.length));
708
- const diagnostic = new vscode.Diagnostic(range, `Invalid JSON body: properties must be wrapped in '{' and '}'.`, vscode.DiagnosticSeverity.Error);
709
- diagnostic.source = 'Norn';
710
- diagnostic.code = 'invalid-json-body-missing-braces';
711
- diagnostics.push(diagnostic);
712
- hasReported = true;
713
- }
714
- objectDepth += countChar(trimmedBodyLine, '{');
715
- objectDepth -= countChar(trimmedBodyLine, '}');
716
- if (objectDepth < 0) {
717
- objectDepth = 0;
718
- }
719
- }
720
- }
721
- // Find empty variable references {{}} or {{ }}
722
- const emptyVarRegex = /\{\{\s*\}\}/g;
723
- let emptyMatch;
724
- while ((emptyMatch = emptyVarRegex.exec(text)) !== null) {
725
- const startPos = document.positionAt(emptyMatch.index);
726
- const endPos = document.positionAt(emptyMatch.index + emptyMatch[0].length);
727
- const range = new vscode.Range(startPos, endPos);
728
- const diagnostic = new vscode.Diagnostic(range, `Empty variable reference`, vscode.DiagnosticSeverity.Error);
729
- diagnostic.source = 'Norn';
730
- diagnostic.code = 'empty-variable';
731
- diagnostics.push(diagnostic);
732
- }
733
- // Check for invalid variable assignments inside sequences
734
- // Valid patterns:
735
- // - var x = "string" (quoted string)
736
- // - var x = 123 (number)
737
- // - var x = true/false/null (literals)
738
- // - var x = varName.path (existing variable path)
739
- // - var x = $1.body.id (response capture)
740
- // - var x = run ... (script/json command)
741
- // - var x = https://... (URL - for backward compat outside sequences)
742
- const varAssignRegex = /^\s*var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
743
- for (let i = 0; i < lines.length; i++) {
744
- const line = lines[i];
745
- // Skip comment lines
746
- if (isCommentLine(line)) {
747
- continue;
748
- }
749
- // Strip inline comments before processing
750
- const lineWithoutComment = stripInlineComment(line);
751
- const match = lineWithoutComment.match(varAssignRegex);
752
- if (match) {
753
- const varName = match[1];
754
- const valueExpr = match[2].trim();
755
- // Skip valid patterns
756
- // 1. Response capture: $1.path
757
- if (/^\$\d+/.test(valueExpr)) {
758
- continue;
759
- }
760
- // 2. Run command: run bash, run js, run powershell, run readJson
761
- if (/^run\s+/i.test(valueExpr)) {
762
- continue;
763
- }
764
- // 3. Quoted string: "..." or '...'
765
- if ((valueExpr.startsWith('"') && valueExpr.endsWith('"')) ||
766
- (valueExpr.startsWith("'") && valueExpr.endsWith("'"))) {
767
- continue;
768
- }
769
- // 4. Number
770
- if (/^-?\d+(\.\d+)?$/.test(valueExpr)) {
771
- continue;
772
- }
773
- // 5. Boolean/null
774
- if (/^(true|false|null)$/i.test(valueExpr)) {
775
- continue;
776
- }
777
- // 6. URL (common case outside sequences)
778
- if (/^https?:\/\//.test(valueExpr)) {
779
- continue;
780
- }
781
- // 7. HTTP request: GET/POST/etc followed by URL or endpoint
782
- const httpRequestMatch = valueExpr.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.+)$/i);
783
- if (httpRequestMatch) {
784
- const afterMethod = httpRequestMatch[2].trim();
785
- // Check if it's a URL (starts with http, https, or {{ - also handle quoted URLs)
786
- if (/^["']?(https?:\/\/|\{\{)/.test(afterMethod)) {
787
- // It's a URL - skip validation
788
- continue;
789
- }
790
- // It might be an endpoint name: EndpointName(params) HeaderGroup1 HeaderGroup2
791
- const endpointMatch = afterMethod.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(?:\([^)]*\))?(.*)$/);
792
- if (endpointMatch) {
793
- const endpointName = endpointMatch[1];
794
- const afterEndpoint = endpointMatch[2]?.trim() || '';
795
- // Validate endpoint exists
796
- if (importedEndpoints.size > 0 && !importedEndpoints.has(endpointName)) {
797
- const endpointStart = line.indexOf(endpointName, line.indexOf('='));
798
- const range = new vscode.Range(new vscode.Position(i, endpointStart), new vscode.Position(i, endpointStart + endpointName.length));
799
- diagnostics.push(new vscode.Diagnostic(range, `Undefined endpoint: '${endpointName}'. Import a .nornapi file that defines this endpoint.`, vscode.DiagnosticSeverity.Error));
800
- }
801
- // Validate header groups
802
- if (afterEndpoint) {
803
- // Strip retry and backoff options before checking header groups
804
- // Use \b word boundaries instead of \s+ since afterEndpoint is already trimmed
805
- const afterEndpointClean = afterEndpoint
806
- .replace(/\bretry\s+\d+/gi, '')
807
- .replace(/\bbackoff\s+\d+(?:\.\d+)?\s*(?:s|ms|seconds?|milliseconds?)?/gi, '');
808
- const headerGroupNames = afterEndpointClean.split(/\s+/).filter(n => n);
809
- for (const hgName of headerGroupNames) {
810
- if (!importedHeaderGroups.has(hgName)) {
811
- const hgStart = line.lastIndexOf(hgName);
812
- const range = new vscode.Range(new vscode.Position(i, hgStart), new vscode.Position(i, hgStart + hgName.length));
813
- diagnostics.push(new vscode.Diagnostic(range, `Undefined header group: '${hgName}'. Import a .nornapi file that defines this header group.`, vscode.DiagnosticSeverity.Error));
814
- }
815
- }
816
- }
817
- }
818
- continue;
819
- }
820
- // 8. Variable path: must be an existing variable
821
- const varPathMatch = valueExpr.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)$/);
822
- if (varPathMatch) {
823
- const refVarName = varPathMatch[1];
824
- // Check if this assignment is inside a sequence
825
- const containingSeq = sequences.find(seq => i > seq.startLine && i < seq.endLine);
826
- let isValid = false;
827
- if (containingSeq) {
828
- // Inside sequence: check global vars + local sequence vars + params
829
- const localVars = getSequenceLocalVariables(containingSeq.content, containingSeq.parameters);
830
- isValid = refVarName in globalVariables || localVars.has(refVarName);
831
- }
832
- else {
833
- // Outside sequences: only global variables
834
- isValid = refVarName in globalVariables;
835
- }
836
- if (isValid) {
837
- continue;
838
- }
839
- // Variable not defined - show error
840
- const valueStart = line.indexOf('=') + 1;
841
- const valueStartTrimmed = valueStart + (line.substring(valueStart).length - line.substring(valueStart).trimStart().length);
842
- const range = new vscode.Range(new vscode.Position(i, valueStartTrimmed), new vscode.Position(i, valueStartTrimmed + refVarName.length));
843
- const diagnostic = new vscode.Diagnostic(range, `Undefined variable: '${refVarName}'. Use quotes for string literals: var ${varName} = "${valueExpr}"`, vscode.DiagnosticSeverity.Error);
844
- diagnostic.source = 'Norn';
845
- diagnostic.code = 'undefined-var-reference';
846
- diagnostics.push(diagnostic);
847
- continue;
848
- }
849
- // If we get here, it's an invalid expression (has spaces, not quoted, not valid pattern)
850
- const valueStart = line.indexOf('=') + 1;
851
- const valueStartTrimmed = valueStart + (line.substring(valueStart).length - line.substring(valueStart).trimStart().length);
852
- const range = new vscode.Range(new vscode.Position(i, valueStartTrimmed), new vscode.Position(i, line.length));
853
- const diagnostic = new vscode.Diagnostic(range, `Invalid variable value. Use quotes for strings: var ${varName} = "${valueExpr}"`, vscode.DiagnosticSeverity.Error);
854
- diagnostic.source = 'Norn';
855
- diagnostic.code = 'invalid-var-value';
856
- diagnostics.push(diagnostic);
857
- }
858
- }
859
- // Find all variable references {{variableName}} or {{variableName.path}}
860
- // Match simple refs and the base name of path refs
861
- const variableRefRegex = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)\}\}/g;
862
- let match;
863
- while ((match = variableRefRegex.exec(text)) !== null) {
864
- const varName = match[1];
865
- const startPos = document.positionAt(match.index);
866
- const lineNumber = startPos.line;
867
- // Skip if this line is a comment
868
- if (isCommentLine(lines[lineNumber])) {
869
- continue;
870
- }
871
- // Check if this reference is inside a sequence
872
- const containingSequence = sequences.find(seq => lineNumber > seq.startLine && lineNumber < seq.endLine);
873
- let isValid = false;
874
- if (containingSequence) {
875
- // Inside a sequence: check global vars + local sequence vars + params
876
- const localVars = getSequenceLocalVariables(containingSequence.content, containingSequence.parameters);
877
- isValid = varName in globalVariables || localVars.has(varName);
878
- }
879
- else {
880
- // Outside sequences: only global variables are available
881
- isValid = varName in globalVariables;
882
- }
883
- if (!isValid) {
884
- const endPos = document.positionAt(match.index + match[0].length);
885
- const range = new vscode.Range(startPos, endPos);
886
- const diagnostic = new vscode.Diagnostic(range, `Undefined variable: '${varName}'`, vscode.DiagnosticSeverity.Error);
887
- diagnostic.source = 'Norn';
888
- diagnostic.code = 'undefined-variable';
889
- diagnostics.push(diagnostic);
890
- }
891
- }
892
- // Check for bare text in print statements that isn't a string, variable, or keyword
893
- const printCommandRegex = /^\s*print\s+(.+)$/i;
894
- const validKeywords = new Set(['true', 'false', 'null']);
895
- for (let i = 0; i < lines.length; i++) {
896
- const line = lines[i];
897
- // Skip comment lines
898
- if (isCommentLine(line)) {
899
- continue;
900
- }
901
- // Strip inline comments before processing print statement
902
- const lineWithoutComment = stripInlineComment(line);
903
- const printMatch = lineWithoutComment.match(printCommandRegex);
904
- if (!printMatch) {
905
- continue;
906
- }
907
- const printContent = printMatch[1];
908
- const printStartCol = line.indexOf(printContent);
909
- // Check if this print is inside a sequence
910
- const containingSeq = sequences.find(seq => i > seq.startLine && i < seq.endLine);
911
- // Get available variables for this scope
912
- let availableVars;
913
- if (containingSeq) {
914
- const localVars = getSequenceLocalVariables(containingSeq.content, containingSeq.parameters);
915
- availableVars = new Set([...Object.keys(globalVariables), ...localVars]);
916
- }
917
- else {
918
- availableVars = new Set(Object.keys(globalVariables));
919
- }
920
- // Parse print content into tokens (respecting quoted strings and operators)
921
- const tokens = tokenizePrintContent(printContent);
922
- // Check for missing concatenation operators between tokens
923
- for (let t = 0; t < tokens.length - 1; t++) {
924
- const currentToken = tokens[t];
925
- const nextToken = tokens[t + 1];
926
- // Skip if current token is an operator
927
- if (/^[+\-*/|&<>=!]+$/.test(currentToken.text)) {
928
- continue;
929
- }
930
- // If next token is not an operator, we're missing concatenation
931
- if (!/^[+\-*/|&<>=!]+$/.test(nextToken.text)) {
932
- // Calculate position between the two tokens
933
- const errorStartCol = printStartCol + currentToken.start + currentToken.text.length;
934
- const errorEndCol = printStartCol + nextToken.start;
935
- const range = new vscode.Range(new vscode.Position(i, errorStartCol), new vscode.Position(i, errorEndCol));
936
- const diagnostic = new vscode.Diagnostic(range, `Missing concatenation operator '+' between '${currentToken.text}' and '${nextToken.text}'`, vscode.DiagnosticSeverity.Error);
937
- diagnostic.source = 'Norn';
938
- diagnostic.code = 'missing-concatenation';
939
- diagnostics.push(diagnostic);
940
- }
941
- }
942
- for (const token of tokens) {
943
- // Skip operators (+, -, etc.)
944
- if (/^[+\-*/|&<>=!]+$/.test(token.text)) {
945
- continue;
946
- }
947
- // Skip quoted strings
948
- if ((token.text.startsWith('"') && token.text.endsWith('"')) ||
949
- (token.text.startsWith("'") && token.text.endsWith("'"))) {
950
- continue;
951
- }
952
- // Skip numbers
953
- if (/^-?\d+(\.\d+)?$/.test(token.text)) {
954
- continue;
955
- }
956
- // Skip keywords (true, false, null)
957
- if (validKeywords.has(token.text.toLowerCase())) {
958
- continue;
959
- }
960
- // Skip variable references {{var}}
961
- if (/^\{\{.+\}\}$/.test(token.text)) {
962
- continue;
963
- }
964
- // Check if it's a valid variable (with optional path)
965
- const varPathMatch = token.text.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)$/);
966
- if (varPathMatch) {
967
- const refVarName = varPathMatch[1];
968
- if (availableVars.has(refVarName)) {
969
- continue;
970
- }
971
- }
972
- // Check if it looks like a plain text word (not a valid identifier pattern)
973
- // Flag words that look like they should be quoted
974
- if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(token.text)) {
975
- // It's a valid identifier but not a defined variable - flag it
976
- const tokenStartCol = printStartCol + token.start;
977
- const range = new vscode.Range(new vscode.Position(i, tokenStartCol), new vscode.Position(i, tokenStartCol + token.text.length));
978
- const diagnostic = new vscode.Diagnostic(range, `Undefined variable: '${token.text}'. Use quotes for string literals: "${token.text}"`, vscode.DiagnosticSeverity.Error);
979
- diagnostic.source = 'Norn';
980
- diagnostic.code = 'undefined-print-variable';
981
- diagnostics.push(diagnostic);
982
- }
983
- else if (!/^[a-zA-Z_]/.test(token.text) === false && !varPathMatch) {
984
- // Some other bare text that doesn't look like anything valid
985
- const tokenStartCol = printStartCol + token.start;
986
- const range = new vscode.Range(new vscode.Position(i, tokenStartCol), new vscode.Position(i, tokenStartCol + token.text.length));
987
- const diagnostic = new vscode.Diagnostic(range, `Invalid expression: '${token.text}'. Use quotes for string literals.`, vscode.DiagnosticSeverity.Error);
988
- diagnostic.source = 'Norn';
989
- diagnostic.code = 'invalid-print-expression';
990
- diagnostics.push(diagnostic);
991
- }
992
- }
993
- }
994
- this.diagnosticCollection.set(document.uri, diagnostics);
995
- }
996
- /**
997
- * Update diagnostics for .nornapi files
998
- */
999
- updateNornapiDiagnostics(document) {
1000
- const diagnostics = [];
1001
- const lines = document.getText().split('\n');
1002
- const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
1003
- let inEndpointsBlock = false;
1004
- for (let i = 0; i < lines.length; i++) {
1005
- const line = lines[i];
1006
- const trimmed = stripInlineComment(line).trim();
1007
- // Skip empty lines/comments
1008
- if (trimmed === '' || trimmed.startsWith('#')) {
1009
- continue;
1010
- }
1011
- // Track endpoints block state
1012
- if (/^endpoints$/i.test(trimmed)) {
1013
- inEndpointsBlock = true;
1014
- continue;
1015
- }
1016
- if (/^end\s+endpoints$/i.test(trimmed)) {
1017
- inEndpointsBlock = false;
1018
- continue;
1019
- }
1020
- if (!inEndpointsBlock) {
1021
- continue;
1022
- }
1023
- // Valid endpoint declaration: Name: METHOD path
1024
- const validEndpoint = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.+)$/i);
1025
- if (validEndpoint) {
1026
- continue;
1027
- }
1028
- const endpointMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/);
1029
- if (!endpointMatch) {
1030
- continue;
1031
- }
1032
- const endpointName = endpointMatch[1];
1033
- const rhs = endpointMatch[2].trim();
1034
- const colonIndex = line.indexOf(':');
1035
- const valueStart = colonIndex >= 0 ? colonIndex + 1 : 0;
1036
- const valueRange = new vscode.Range(new vscode.Position(i, Math.max(0, valueStart)), new vscode.Position(i, line.length));
1037
- // While typing: "Name:" or "Name: G" should not error yet
1038
- if (rhs === '') {
1039
- continue;
1040
- }
1041
- const firstToken = rhs.split(/\s+/)[0];
1042
- const upperFirstToken = firstToken.toUpperCase();
1043
- const isPartialMethod = httpMethods.some(m => m.startsWith(upperFirstToken));
1044
- if (!rhs.includes(' ') && isPartialMethod) {
1045
- continue;
1046
- }
1047
- if (httpMethods.includes(upperFirstToken) && !rhs.includes(' ')) {
1048
- const diagnostic = new vscode.Diagnostic(valueRange, `Endpoint '${endpointName}' is missing a URL/path after method. Use: ${endpointName}: ${upperFirstToken} /path`, vscode.DiagnosticSeverity.Error);
1049
- diagnostic.source = 'Norn';
1050
- diagnostic.code = 'missing-endpoint-path';
1051
- diagnostics.push(diagnostic);
1052
- continue;
1053
- }
1054
- const diagnostic = new vscode.Diagnostic(valueRange, `Endpoint '${endpointName}' is missing HTTP method. Use: ${endpointName}: GET /path`, vscode.DiagnosticSeverity.Error);
1055
- diagnostic.source = 'Norn';
1056
- diagnostic.code = 'missing-endpoint-method';
1057
- diagnostics.push(diagnostic);
1058
- }
1059
- this.diagnosticCollection.set(document.uri, diagnostics);
1060
- }
1061
- /**
1062
- * Update diagnostics for .nornenv files
1063
- */
1064
- updateNornenvDiagnostics(document) {
1065
- const diagnostics = [];
1066
- const text = document.getText();
1067
- const lines = text.split('\n');
1068
- for (let i = 0; i < lines.length; i++) {
1069
- const line = lines[i];
1070
- const trimmed = line.trim();
1071
- // Skip comments and empty lines
1072
- if (trimmed === '' || trimmed.startsWith('#')) {
1073
- continue;
1074
- }
1075
- // Check for invalid "secret var" pattern
1076
- const secretVarMatch = trimmed.match(/^(secret)\s+(var)\s*/i);
1077
- if (secretVarMatch) {
1078
- const varStart = line.indexOf('var', line.indexOf('secret') + 6);
1079
- const range = new vscode.Range(new vscode.Position(i, varStart), new vscode.Position(i, varStart + 3));
1080
- const diagnostic = new vscode.Diagnostic(range, `Invalid syntax: 'var' is not allowed after 'secret'. Use 'secret variableName = value'.`, vscode.DiagnosticSeverity.Error);
1081
- diagnostic.source = 'Norn';
1082
- diagnostic.code = 'invalid-secret-var';
1083
- diagnostics.push(diagnostic);
1084
- continue;
1085
- }
1086
- // Check for valid patterns - if none match, it's an error
1087
- const validPatterns = [
1088
- /^\[env:[a-zA-Z_][a-zA-Z0-9_-]*\]$/, // Environment section
1089
- /^(secret|var)\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*.+$/, // Complete declaration
1090
- /^(secret|var)\s*$/, // Just keyword (while typing)
1091
- /^(secret|var)\s+[a-zA-Z_][a-zA-Z0-9_]*\s*$/, // Keyword + name (while typing)
1092
- /^(secret|var)\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*$/, // Keyword + name + = (while typing)
1093
- ];
1094
- const isValid = validPatterns.some(pattern => pattern.test(trimmed));
1095
- if (!isValid) {
1096
- const range = new vscode.Range(new vscode.Position(i, 0), new vscode.Position(i, line.length));
1097
- const diagnostic = new vscode.Diagnostic(range, `Invalid syntax. Expected: 'var name = value', 'secret name = value', or '[env:name]'.`, vscode.DiagnosticSeverity.Error);
1098
- diagnostic.source = 'Norn';
1099
- diagnostic.code = 'invalid-nornenv-syntax';
1100
- diagnostics.push(diagnostic);
1101
- }
1102
- }
1103
- this.diagnosticCollection.set(document.uri, diagnostics);
1104
- }
1105
- clearDiagnostics(document) {
1106
- this.diagnosticCollection.delete(document.uri);
1107
- }
1108
- dispose() {
1109
- this.diagnosticCollection.dispose();
1110
- }
1111
- }
1112
- exports.DiagnosticProvider = DiagnosticProvider;
1113
- //# sourceMappingURL=diagnosticProvider.js.map