norn-cli 1.3.16 → 1.3.18

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.
@@ -0,0 +1,980 @@
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
+ /**
104
+ * Tokenize print statement content into individual tokens.
105
+ * Respects quoted strings and splits on operators/whitespace.
106
+ */
107
+ function tokenizePrintContent(content) {
108
+ const tokens = [];
109
+ let current = '';
110
+ let currentStart = 0;
111
+ let inString = false;
112
+ let stringChar = '';
113
+ let i = 0;
114
+ const pushToken = () => {
115
+ if (current.trim()) {
116
+ tokens.push({ text: current.trim(), start: currentStart + (current.length - current.trimStart().length) });
117
+ }
118
+ current = '';
119
+ };
120
+ while (i < content.length) {
121
+ const char = content[i];
122
+ if (inString) {
123
+ current += char;
124
+ if (char === stringChar && content[i - 1] !== '\\') {
125
+ inString = false;
126
+ pushToken();
127
+ }
128
+ i++;
129
+ continue;
130
+ }
131
+ // Start of string
132
+ if (char === '"' || char === "'") {
133
+ pushToken();
134
+ inString = true;
135
+ stringChar = char;
136
+ current = char;
137
+ currentStart = i;
138
+ i++;
139
+ continue;
140
+ }
141
+ // Operators and whitespace are delimiters
142
+ if (/[\s+\-*/|&<>=!]/.test(char)) {
143
+ pushToken();
144
+ // If it's an operator, add it as its own token
145
+ if (/[+\-*/|&<>=!]/.test(char)) {
146
+ tokens.push({ text: char, start: i });
147
+ }
148
+ currentStart = i + 1;
149
+ i++;
150
+ continue;
151
+ }
152
+ // Regular character
153
+ if (current === '') {
154
+ currentStart = i;
155
+ }
156
+ current += char;
157
+ i++;
158
+ }
159
+ // Push any remaining token
160
+ pushToken();
161
+ return tokens;
162
+ }
163
+ class DiagnosticProvider {
164
+ diagnosticCollection;
165
+ constructor() {
166
+ this.diagnosticCollection = vscode.languages.createDiagnosticCollection('norn');
167
+ }
168
+ updateDiagnostics(document) {
169
+ if (document.languageId === 'nornenv') {
170
+ this.updateNornenvDiagnostics(document);
171
+ return;
172
+ }
173
+ if (document.languageId === 'nornapi') {
174
+ this.updateNornapiDiagnostics(document);
175
+ return;
176
+ }
177
+ if (document.languageId !== 'norn') {
178
+ return;
179
+ }
180
+ const diagnostics = [];
181
+ const text = document.getText();
182
+ // Extract file-level variables (outside sequences) + environment
183
+ const fileLevelVariables = (0, parser_1.extractFileLevelVariables)(text);
184
+ const envVariables = (0, environmentProvider_1.getEnvironmentVariables)();
185
+ const globalVariables = { ...envVariables, ...fileLevelVariables };
186
+ // Extract sequences for scope-aware checking
187
+ const sequences = (0, sequenceRunner_1.extractSequences)(text);
188
+ const lines = text.split('\n');
189
+ // Check for duplicate sequence declarations
190
+ const declaredSequences = new Map(); // seqName -> first declaration line
191
+ for (const seq of sequences) {
192
+ if (declaredSequences.has(seq.name)) {
193
+ const firstLine = declaredSequences.get(seq.name);
194
+ const line = lines[seq.startLine];
195
+ // Find the sequence name position in the line
196
+ const nameStart = line.indexOf(seq.name);
197
+ const range = new vscode.Range(new vscode.Position(seq.startLine, nameStart), new vscode.Position(seq.startLine, nameStart + seq.name.length));
198
+ const diagnostic = new vscode.Diagnostic(range, `Duplicate sequence: '${seq.name}' is already declared on line ${firstLine + 1}.`, vscode.DiagnosticSeverity.Error);
199
+ diagnostic.source = 'Norn';
200
+ diagnostic.code = 'duplicate-sequence';
201
+ diagnostics.push(diagnostic);
202
+ }
203
+ else {
204
+ declaredSequences.set(seq.name, seq.startLine);
205
+ }
206
+ }
207
+ // Check for duplicate variable declarations at file level (outside sequences)
208
+ const varDeclRegex = /^\s*var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=/;
209
+ const declaredVars = new Map(); // varName -> first declaration line
210
+ for (let i = 0; i < lines.length; i++) {
211
+ const line = lines[i];
212
+ // Skip comment lines
213
+ if (isCommentLine(line)) {
214
+ continue;
215
+ }
216
+ // Check if this line is inside a sequence (skip sequence-local vars)
217
+ const isInsideSequence = sequences.some(seq => i > seq.startLine && i < seq.endLine);
218
+ if (isInsideSequence) {
219
+ continue;
220
+ }
221
+ const lineWithoutComment = stripInlineComment(line);
222
+ const varMatch = lineWithoutComment.match(varDeclRegex);
223
+ if (varMatch) {
224
+ const varName = varMatch[1];
225
+ if (declaredVars.has(varName)) {
226
+ const firstLine = declaredVars.get(varName);
227
+ const startCol = line.indexOf(varName);
228
+ const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, startCol + varName.length));
229
+ const diagnostic = new vscode.Diagnostic(range, `Duplicate variable declaration: '${varName}' is already declared on line ${firstLine + 1}.`, vscode.DiagnosticSeverity.Error);
230
+ diagnostic.source = 'Norn';
231
+ diagnostic.code = 'duplicate-variable';
232
+ diagnostics.push(diagnostic);
233
+ }
234
+ else {
235
+ declaredVars.set(varName, i);
236
+ }
237
+ }
238
+ }
239
+ // Check for invalid named request declarations (names with spaces) and duplicates
240
+ const invalidNameRegex = /^\s*\[(?:Name:\s*)?(.+)\]\s*$/;
241
+ const namedRequestRegex = /^\s*\[(?:Name:\s*)?([a-zA-Z_][a-zA-Z0-9_-]*)\]/;
242
+ const validNameRegex = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
243
+ const declaredNamedRequests = new Map(); // name -> first declaration line
244
+ for (let i = 0; i < lines.length; i++) {
245
+ const line = lines[i];
246
+ // Skip comment lines
247
+ if (isCommentLine(line)) {
248
+ continue;
249
+ }
250
+ // Strip inline comments before checking
251
+ const lineWithoutComment = stripInlineComment(line);
252
+ // Check for valid named request and track for duplicates
253
+ const namedMatch = lineWithoutComment.match(namedRequestRegex);
254
+ if (namedMatch) {
255
+ const name = namedMatch[1];
256
+ if (declaredNamedRequests.has(name)) {
257
+ const firstLine = declaredNamedRequests.get(name);
258
+ const startCol = line.indexOf('[');
259
+ const endCol = line.indexOf(']') + 1;
260
+ const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, endCol));
261
+ const diagnostic = new vscode.Diagnostic(range, `Duplicate named request: '${name}' is already declared on line ${firstLine + 1}.`, vscode.DiagnosticSeverity.Error);
262
+ diagnostic.source = 'Norn';
263
+ diagnostic.code = 'duplicate-named-request';
264
+ diagnostics.push(diagnostic);
265
+ }
266
+ else {
267
+ declaredNamedRequests.set(name, i);
268
+ }
269
+ }
270
+ // Check for invalid names (with spaces etc)
271
+ const match = lineWithoutComment.match(invalidNameRegex);
272
+ if (match) {
273
+ const name = match[1].trim();
274
+ if (!validNameRegex.test(name)) {
275
+ const startCol = line.indexOf('[');
276
+ const endCol = line.indexOf(']') + 1;
277
+ const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, endCol));
278
+ 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);
279
+ diagnostic.source = 'Norn';
280
+ diagnostic.code = 'invalid-request-name';
281
+ diagnostics.push(diagnostic);
282
+ }
283
+ }
284
+ }
285
+ // Check for import statement errors
286
+ const imports = (0, parser_1.extractImports)(text);
287
+ const documentDir = path.dirname(document.uri.fsPath);
288
+ // Track imported names with source info for duplicate detection
289
+ // Maps name (lowercase) -> { sourcePath, lineNumber }
290
+ const importedRequests = new Map();
291
+ const importedSequences = new Map();
292
+ // Track imported API definitions (endpoints and header groups from .nornapi files)
293
+ // Maps name -> { sourcePath, lineNumber } to detect duplicates
294
+ const importedEndpoints = new Map();
295
+ const importedHeaderGroups = new Map();
296
+ for (const imp of imports) {
297
+ const absolutePath = path.resolve(documentDir, imp.path);
298
+ // Check if file exists
299
+ if (!fs.existsSync(absolutePath)) {
300
+ const line = lines[imp.lineNumber];
301
+ const startCol = line.indexOf(imp.path);
302
+ const range = new vscode.Range(new vscode.Position(imp.lineNumber, startCol), new vscode.Position(imp.lineNumber, startCol + imp.path.length));
303
+ const diagnostic = new vscode.Diagnostic(range, `Import file not found: '${imp.path}'`, vscode.DiagnosticSeverity.Error);
304
+ diagnostic.source = 'Norn';
305
+ diagnostic.code = 'import-not-found';
306
+ diagnostics.push(diagnostic);
307
+ }
308
+ else {
309
+ // Read the imported file and extract its named requests/sequences
310
+ try {
311
+ const importedContent = fs.readFileSync(absolutePath, 'utf-8');
312
+ // Check if this is a .nornapi file
313
+ if (imp.path.endsWith('.nornapi')) {
314
+ const apiDef = (0, nornapiParser_1.parseNornApiFile)(importedContent);
315
+ for (const endpoint of apiDef.endpoints) {
316
+ const existing = importedEndpoints.get(endpoint.name);
317
+ if (existing) {
318
+ // Duplicate endpoint found
319
+ const line = lines[imp.lineNumber];
320
+ const startCol = line.indexOf(imp.path);
321
+ const range = new vscode.Range(new vscode.Position(imp.lineNumber, startCol), new vscode.Position(imp.lineNumber, startCol + imp.path.length));
322
+ const diagnostic = new vscode.Diagnostic(range, `Duplicate endpoint '${endpoint.name}': already defined in '${existing.sourcePath}'`, vscode.DiagnosticSeverity.Error);
323
+ diagnostic.source = 'Norn';
324
+ diagnostic.code = 'duplicate-endpoint';
325
+ diagnostics.push(diagnostic);
326
+ }
327
+ else {
328
+ importedEndpoints.set(endpoint.name, { sourcePath: imp.path, lineNumber: imp.lineNumber });
329
+ }
330
+ }
331
+ for (const group of apiDef.headerGroups) {
332
+ const existing = importedHeaderGroups.get(group.name);
333
+ if (existing) {
334
+ // Duplicate header group found
335
+ const line = lines[imp.lineNumber];
336
+ const startCol = line.indexOf(imp.path);
337
+ const range = new vscode.Range(new vscode.Position(imp.lineNumber, startCol), new vscode.Position(imp.lineNumber, startCol + imp.path.length));
338
+ const diagnostic = new vscode.Diagnostic(range, `Duplicate header group '${group.name}': already defined in '${existing.sourcePath}'`, vscode.DiagnosticSeverity.Error);
339
+ diagnostic.source = 'Norn';
340
+ diagnostic.code = 'duplicate-header-group';
341
+ diagnostics.push(diagnostic);
342
+ }
343
+ else {
344
+ importedHeaderGroups.set(group.name, { sourcePath: imp.path, lineNumber: imp.lineNumber });
345
+ }
346
+ }
347
+ }
348
+ else {
349
+ // Regular .norn file
350
+ const fileRequests = (0, parser_1.extractNamedRequests)(importedContent);
351
+ const fileSequences = (0, sequenceRunner_1.extractSequences)(importedContent);
352
+ // Check for duplicate named requests
353
+ for (const req of fileRequests) {
354
+ const lowerName = req.name.toLowerCase();
355
+ const existing = importedRequests.get(lowerName);
356
+ if (existing) {
357
+ const line = lines[imp.lineNumber];
358
+ const startCol = line.indexOf(imp.path);
359
+ const range = new vscode.Range(new vscode.Position(imp.lineNumber, startCol), new vscode.Position(imp.lineNumber, startCol + imp.path.length));
360
+ const diagnostic = new vscode.Diagnostic(range, `Duplicate named request '${req.name}': already defined in '${existing.sourcePath}'`, vscode.DiagnosticSeverity.Error);
361
+ diagnostic.source = 'Norn';
362
+ diagnostic.code = 'duplicate-named-request-import';
363
+ diagnostics.push(diagnostic);
364
+ }
365
+ else {
366
+ importedRequests.set(lowerName, { sourcePath: imp.path, lineNumber: imp.lineNumber });
367
+ }
368
+ }
369
+ // Check for duplicate sequences
370
+ for (const seq of fileSequences) {
371
+ const lowerName = seq.name.toLowerCase();
372
+ const existing = importedSequences.get(lowerName);
373
+ if (existing) {
374
+ const line = lines[imp.lineNumber];
375
+ const startCol = line.indexOf(imp.path);
376
+ const range = new vscode.Range(new vscode.Position(imp.lineNumber, startCol), new vscode.Position(imp.lineNumber, startCol + imp.path.length));
377
+ const diagnostic = new vscode.Diagnostic(range, `Duplicate sequence '${seq.name}': already defined in '${existing.sourcePath}'`, vscode.DiagnosticSeverity.Error);
378
+ diagnostic.source = 'Norn';
379
+ diagnostic.code = 'duplicate-sequence-import';
380
+ diagnostics.push(diagnostic);
381
+ }
382
+ else {
383
+ importedSequences.set(lowerName, { sourcePath: imp.path, lineNumber: imp.lineNumber });
384
+ }
385
+ }
386
+ }
387
+ }
388
+ catch {
389
+ // Ignore read errors - file exists but couldn't be read
390
+ }
391
+ }
392
+ }
393
+ // Check for misplaced tags/annotations.
394
+ // Any @decorator line must be directly associated with a following sequence declaration.
395
+ const tagPattern = /^\s*@([a-zA-Z_][a-zA-Z0-9_-]*)(?:\(([^)]*)\))?/;
396
+ const sequencePattern = /^\s*(?:test\s+)?sequence\s+[a-zA-Z_][a-zA-Z0-9_-]*/;
397
+ for (let i = 0; i < lines.length; i++) {
398
+ const line = lines[i];
399
+ // Skip comment lines
400
+ if (isCommentLine(line)) {
401
+ continue;
402
+ }
403
+ // Check if this line contains a tag/annotation decorator
404
+ if (tagPattern.test(line)) {
405
+ // Look ahead to find what this decorator applies to
406
+ let foundSequence = false;
407
+ let j = i + 1;
408
+ // Skip empty lines and additional decorator lines
409
+ while (j < lines.length) {
410
+ const nextLine = lines[j].trim();
411
+ if (!nextLine) {
412
+ j++;
413
+ continue;
414
+ }
415
+ if (tagPattern.test(nextLine)) {
416
+ j++;
417
+ continue;
418
+ }
419
+ if (sequencePattern.test(nextLine)) {
420
+ foundSequence = true;
421
+ }
422
+ break;
423
+ }
424
+ const tagMatch = line.match(/@([a-zA-Z_][a-zA-Z0-9_-]*)(?:\([^)]+\))?/);
425
+ if (tagMatch) {
426
+ const startCol = line.indexOf('@');
427
+ const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, startCol + tagMatch[0].length));
428
+ if (!foundSequence) {
429
+ const annotationName = tagMatch[1];
430
+ const isDataAnnotation = annotationName === 'data' || annotationName === 'theory';
431
+ const message = isDataAnnotation
432
+ ? `@${annotationName} must be placed immediately before a sequence declaration.`
433
+ : `Tags must be placed immediately before a sequence declaration.`;
434
+ const diagnostic = new vscode.Diagnostic(range, message, vscode.DiagnosticSeverity.Error);
435
+ diagnostic.source = 'Norn';
436
+ diagnostic.code = 'misplaced-tag';
437
+ diagnostics.push(diagnostic);
438
+ }
439
+ }
440
+ }
441
+ }
442
+ // Check for test sequences with required parameters but no @data/@theory
443
+ for (const seq of sequences) {
444
+ if (seq.isTest) {
445
+ const requiredParams = seq.parameters.filter(p => p.defaultValue === undefined);
446
+ if (requiredParams.length > 0 && !seq.theoryData) {
447
+ // Find the line with the 'test sequence' declaration
448
+ let declarationLine = seq.startLine;
449
+ for (let i = seq.startLine; i <= seq.endLine; i++) {
450
+ if (/^\s*test\s+sequence\s+/.test(lines[i])) {
451
+ declarationLine = i;
452
+ break;
453
+ }
454
+ }
455
+ const line = lines[declarationLine];
456
+ const startCol = line.indexOf('test');
457
+ const endCol = line.length;
458
+ const range = new vscode.Range(new vscode.Position(declarationLine, startCol), new vscode.Position(declarationLine, endCol));
459
+ const paramNames = requiredParams.map(p => p.name).join(', ');
460
+ 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);
461
+ diagnostic.source = 'Norn';
462
+ diagnostic.code = 'test-missing-data';
463
+ diagnostics.push(diagnostic);
464
+ }
465
+ }
466
+ }
467
+ // Check for undefined named requests/sequences in 'run' commands
468
+ const namedRequests = (0, parser_1.extractNamedRequests)(text);
469
+ const namedRequestNames = new Set(namedRequests.map(r => r.name.toLowerCase()));
470
+ const sequenceNames = new Set(sequences.map(s => s.name.toLowerCase()));
471
+ // Build a map of sequence name -> required parameter count
472
+ const sequenceParamInfo = new Map();
473
+ for (const seq of sequences) {
474
+ const requiredParams = seq.parameters.filter(p => p.defaultValue === undefined).length;
475
+ sequenceParamInfo.set(seq.name.toLowerCase(), { required: requiredParams, total: seq.parameters.length });
476
+ }
477
+ // Updated regex to capture optional arguments: run Name or run Name(args)
478
+ const runCommandRegex = /^\s*(?:var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)?run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\(([^)]*)\))?\s*$/;
479
+ for (let i = 0; i < lines.length; i++) {
480
+ const line = lines[i];
481
+ // Skip comment lines
482
+ if (isCommentLine(line)) {
483
+ continue;
484
+ }
485
+ const match = line.match(runCommandRegex);
486
+ if (match) {
487
+ const requestName = match[1];
488
+ const argsStr = match[2]; // undefined if no parens, empty string if (), or "arg1, arg2"
489
+ const lowerName = requestName.toLowerCase();
490
+ // Skip if it's a script command
491
+ if (['bash', 'js', 'powershell'].includes(lowerName)) {
492
+ continue;
493
+ }
494
+ // Check if it's a local named request, local sequence, or imported
495
+ const isLocalRequest = namedRequestNames.has(lowerName);
496
+ const isLocalSequence = sequenceNames.has(lowerName);
497
+ const isImportedRequest = importedRequests.has(lowerName);
498
+ const isImportedSequence = importedSequences.has(lowerName);
499
+ if (!isLocalRequest && !isLocalSequence && !isImportedRequest && !isImportedSequence) {
500
+ const startCol = line.indexOf(requestName);
501
+ const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, startCol + requestName.length));
502
+ const diagnostic = new vscode.Diagnostic(range, `Undefined request or sequence: '${requestName}'`, vscode.DiagnosticSeverity.Error);
503
+ diagnostic.source = 'Norn';
504
+ diagnostic.code = 'undefined-request';
505
+ diagnostics.push(diagnostic);
506
+ }
507
+ else if (isLocalSequence) {
508
+ // Check if sequence requires parameters but none were provided
509
+ const paramInfo = sequenceParamInfo.get(lowerName);
510
+ if (paramInfo && paramInfo.required > 0) {
511
+ // Count provided arguments
512
+ let providedCount = 0;
513
+ if (argsStr !== undefined && argsStr.trim() !== '') {
514
+ // Split by comma, but respect quoted strings
515
+ const args = argsStr.split(',').map(a => a.trim()).filter(a => a !== '');
516
+ providedCount = args.length;
517
+ }
518
+ if (providedCount < paramInfo.required) {
519
+ const startCol = line.indexOf(requestName);
520
+ const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, startCol + requestName.length));
521
+ const seq = sequences.find(s => s.name.toLowerCase() === lowerName);
522
+ const requiredParamNames = seq ?
523
+ seq.parameters.filter(p => p.defaultValue === undefined).map(p => p.name).join(', ') :
524
+ '';
525
+ const diagnostic = new vscode.Diagnostic(range, `Sequence '${requestName}' requires ${paramInfo.required} parameter(s): ${requiredParamNames}`, vscode.DiagnosticSeverity.Error);
526
+ diagnostic.source = 'Norn';
527
+ diagnostic.code = 'missing-sequence-parameters';
528
+ diagnostics.push(diagnostic);
529
+ }
530
+ }
531
+ }
532
+ }
533
+ }
534
+ // Check for undefined API endpoints and header groups
535
+ // Match patterns like: GET EndpointName("arg") HeaderGroup
536
+ const apiRequestRegex = /^\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([A-Z][a-zA-Z0-9_]*)(?:\([^)]*\))?(?:\s+([A-Z][a-zA-Z0-9_]*))?/;
537
+ // Match standalone header group on its own line (after an API request)
538
+ const standaloneHeaderGroupRegex = /^\s+([A-Z][a-zA-Z0-9_]*)\s*$/;
539
+ for (let i = 0; i < lines.length; i++) {
540
+ const line = lines[i];
541
+ // Skip comment lines
542
+ if (isCommentLine(line)) {
543
+ continue;
544
+ }
545
+ // Strip inline comments before checking API request syntax
546
+ const lineWithoutComment = stripInlineComment(line);
547
+ // Check API request syntax
548
+ const apiMatch = lineWithoutComment.match(apiRequestRegex);
549
+ if (apiMatch) {
550
+ const endpointName = apiMatch[2];
551
+ const headerGroupName = apiMatch[3];
552
+ // Check if endpoint exists
553
+ if (!importedEndpoints.has(endpointName)) {
554
+ const startCol = line.indexOf(endpointName);
555
+ const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, startCol + endpointName.length));
556
+ const diagnostic = new vscode.Diagnostic(range, `Undefined endpoint: '${endpointName}'. Import a .nornapi file that defines this endpoint.`, vscode.DiagnosticSeverity.Error);
557
+ diagnostic.source = 'Norn';
558
+ diagnostic.code = 'undefined-endpoint';
559
+ diagnostics.push(diagnostic);
560
+ }
561
+ // Check if header group exists (if specified)
562
+ if (headerGroupName && !importedHeaderGroups.has(headerGroupName)) {
563
+ const startCol = line.lastIndexOf(headerGroupName);
564
+ const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, startCol + headerGroupName.length));
565
+ const diagnostic = new vscode.Diagnostic(range, `Undefined header group: '${headerGroupName}'. Import a .nornapi file that defines this header group.`, vscode.DiagnosticSeverity.Error);
566
+ diagnostic.source = 'Norn';
567
+ diagnostic.code = 'undefined-header-group';
568
+ diagnostics.push(diagnostic);
569
+ }
570
+ }
571
+ // Check standalone header group lines (indented, capitalized identifier only)
572
+ const standaloneMatch = lineWithoutComment.match(standaloneHeaderGroupRegex);
573
+ if (standaloneMatch) {
574
+ const headerGroupName = standaloneMatch[1];
575
+ // Only flag as undefined if we have some .nornapi imports (otherwise it might just be a typo or URL)
576
+ if (importedEndpoints.size > 0 || importedHeaderGroups.size > 0) {
577
+ if (!importedHeaderGroups.has(headerGroupName)) {
578
+ const startCol = line.indexOf(headerGroupName);
579
+ const range = new vscode.Range(new vscode.Position(i, startCol), new vscode.Position(i, startCol + headerGroupName.length));
580
+ const diagnostic = new vscode.Diagnostic(range, `Undefined header group: '${headerGroupName}'. Import a .nornapi file that defines this header group.`, vscode.DiagnosticSeverity.Error);
581
+ diagnostic.source = 'Norn';
582
+ diagnostic.code = 'undefined-header-group';
583
+ diagnostics.push(diagnostic);
584
+ }
585
+ }
586
+ }
587
+ }
588
+ // Find empty variable references {{}} or {{ }}
589
+ const emptyVarRegex = /\{\{\s*\}\}/g;
590
+ let emptyMatch;
591
+ while ((emptyMatch = emptyVarRegex.exec(text)) !== null) {
592
+ const startPos = document.positionAt(emptyMatch.index);
593
+ const endPos = document.positionAt(emptyMatch.index + emptyMatch[0].length);
594
+ const range = new vscode.Range(startPos, endPos);
595
+ const diagnostic = new vscode.Diagnostic(range, `Empty variable reference`, vscode.DiagnosticSeverity.Error);
596
+ diagnostic.source = 'Norn';
597
+ diagnostic.code = 'empty-variable';
598
+ diagnostics.push(diagnostic);
599
+ }
600
+ // Check for invalid variable assignments inside sequences
601
+ // Valid patterns:
602
+ // - var x = "string" (quoted string)
603
+ // - var x = 123 (number)
604
+ // - var x = true/false/null (literals)
605
+ // - var x = varName.path (existing variable path)
606
+ // - var x = $1.body.id (response capture)
607
+ // - var x = run ... (script/json command)
608
+ // - var x = https://... (URL - for backward compat outside sequences)
609
+ const varAssignRegex = /^\s*var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
610
+ for (let i = 0; i < lines.length; i++) {
611
+ const line = lines[i];
612
+ // Skip comment lines
613
+ if (isCommentLine(line)) {
614
+ continue;
615
+ }
616
+ // Strip inline comments before processing
617
+ const lineWithoutComment = stripInlineComment(line);
618
+ const match = lineWithoutComment.match(varAssignRegex);
619
+ if (match) {
620
+ const varName = match[1];
621
+ const valueExpr = match[2].trim();
622
+ // Skip valid patterns
623
+ // 1. Response capture: $1.path
624
+ if (/^\$\d+/.test(valueExpr)) {
625
+ continue;
626
+ }
627
+ // 2. Run command: run bash, run js, run powershell, run readJson
628
+ if (/^run\s+/i.test(valueExpr)) {
629
+ continue;
630
+ }
631
+ // 3. Quoted string: "..." or '...'
632
+ if ((valueExpr.startsWith('"') && valueExpr.endsWith('"')) ||
633
+ (valueExpr.startsWith("'") && valueExpr.endsWith("'"))) {
634
+ continue;
635
+ }
636
+ // 4. Number
637
+ if (/^-?\d+(\.\d+)?$/.test(valueExpr)) {
638
+ continue;
639
+ }
640
+ // 5. Boolean/null
641
+ if (/^(true|false|null)$/i.test(valueExpr)) {
642
+ continue;
643
+ }
644
+ // 6. URL (common case outside sequences)
645
+ if (/^https?:\/\//.test(valueExpr)) {
646
+ continue;
647
+ }
648
+ // 7. HTTP request: GET/POST/etc followed by URL or endpoint
649
+ const httpRequestMatch = valueExpr.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.+)$/i);
650
+ if (httpRequestMatch) {
651
+ const afterMethod = httpRequestMatch[2].trim();
652
+ // Check if it's a URL (starts with http, https, or {{ - also handle quoted URLs)
653
+ if (/^["']?(https?:\/\/|\{\{)/.test(afterMethod)) {
654
+ // It's a URL - skip validation
655
+ continue;
656
+ }
657
+ // It might be an endpoint name: EndpointName(params) HeaderGroup1 HeaderGroup2
658
+ const endpointMatch = afterMethod.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(?:\([^)]*\))?(.*)$/);
659
+ if (endpointMatch) {
660
+ const endpointName = endpointMatch[1];
661
+ const afterEndpoint = endpointMatch[2]?.trim() || '';
662
+ // Validate endpoint exists
663
+ if (importedEndpoints.size > 0 && !importedEndpoints.has(endpointName)) {
664
+ const endpointStart = line.indexOf(endpointName, line.indexOf('='));
665
+ const range = new vscode.Range(new vscode.Position(i, endpointStart), new vscode.Position(i, endpointStart + endpointName.length));
666
+ diagnostics.push(new vscode.Diagnostic(range, `Undefined endpoint: '${endpointName}'. Import a .nornapi file that defines this endpoint.`, vscode.DiagnosticSeverity.Error));
667
+ }
668
+ // Validate header groups
669
+ if (afterEndpoint) {
670
+ // Strip retry and backoff options before checking header groups
671
+ // Use \b word boundaries instead of \s+ since afterEndpoint is already trimmed
672
+ const afterEndpointClean = afterEndpoint
673
+ .replace(/\bretry\s+\d+/gi, '')
674
+ .replace(/\bbackoff\s+\d+(?:\.\d+)?\s*(?:s|ms|seconds?|milliseconds?)?/gi, '');
675
+ const headerGroupNames = afterEndpointClean.split(/\s+/).filter(n => n);
676
+ for (const hgName of headerGroupNames) {
677
+ if (!importedHeaderGroups.has(hgName)) {
678
+ const hgStart = line.lastIndexOf(hgName);
679
+ const range = new vscode.Range(new vscode.Position(i, hgStart), new vscode.Position(i, hgStart + hgName.length));
680
+ diagnostics.push(new vscode.Diagnostic(range, `Undefined header group: '${hgName}'. Import a .nornapi file that defines this header group.`, vscode.DiagnosticSeverity.Error));
681
+ }
682
+ }
683
+ }
684
+ }
685
+ continue;
686
+ }
687
+ // 8. Variable path: must be an existing variable
688
+ const varPathMatch = valueExpr.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)$/);
689
+ if (varPathMatch) {
690
+ const refVarName = varPathMatch[1];
691
+ // Check if this assignment is inside a sequence
692
+ const containingSeq = sequences.find(seq => i > seq.startLine && i < seq.endLine);
693
+ let isValid = false;
694
+ if (containingSeq) {
695
+ // Inside sequence: check global vars + local sequence vars + params
696
+ const localVars = getSequenceLocalVariables(containingSeq.content, containingSeq.parameters);
697
+ isValid = refVarName in globalVariables || localVars.has(refVarName);
698
+ }
699
+ else {
700
+ // Outside sequences: only global variables
701
+ isValid = refVarName in globalVariables;
702
+ }
703
+ if (isValid) {
704
+ continue;
705
+ }
706
+ // Variable not defined - show error
707
+ const valueStart = line.indexOf('=') + 1;
708
+ const valueStartTrimmed = valueStart + (line.substring(valueStart).length - line.substring(valueStart).trimStart().length);
709
+ const range = new vscode.Range(new vscode.Position(i, valueStartTrimmed), new vscode.Position(i, valueStartTrimmed + refVarName.length));
710
+ const diagnostic = new vscode.Diagnostic(range, `Undefined variable: '${refVarName}'. Use quotes for string literals: var ${varName} = "${valueExpr}"`, vscode.DiagnosticSeverity.Error);
711
+ diagnostic.source = 'Norn';
712
+ diagnostic.code = 'undefined-var-reference';
713
+ diagnostics.push(diagnostic);
714
+ continue;
715
+ }
716
+ // If we get here, it's an invalid expression (has spaces, not quoted, not valid pattern)
717
+ const valueStart = line.indexOf('=') + 1;
718
+ const valueStartTrimmed = valueStart + (line.substring(valueStart).length - line.substring(valueStart).trimStart().length);
719
+ const range = new vscode.Range(new vscode.Position(i, valueStartTrimmed), new vscode.Position(i, line.length));
720
+ const diagnostic = new vscode.Diagnostic(range, `Invalid variable value. Use quotes for strings: var ${varName} = "${valueExpr}"`, vscode.DiagnosticSeverity.Error);
721
+ diagnostic.source = 'Norn';
722
+ diagnostic.code = 'invalid-var-value';
723
+ diagnostics.push(diagnostic);
724
+ }
725
+ }
726
+ // Find all variable references {{variableName}} or {{variableName.path}}
727
+ // Match simple refs and the base name of path refs
728
+ const variableRefRegex = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)\}\}/g;
729
+ let match;
730
+ while ((match = variableRefRegex.exec(text)) !== null) {
731
+ const varName = match[1];
732
+ const startPos = document.positionAt(match.index);
733
+ const lineNumber = startPos.line;
734
+ // Skip if this line is a comment
735
+ if (isCommentLine(lines[lineNumber])) {
736
+ continue;
737
+ }
738
+ // Check if this reference is inside a sequence
739
+ const containingSequence = sequences.find(seq => lineNumber > seq.startLine && lineNumber < seq.endLine);
740
+ let isValid = false;
741
+ if (containingSequence) {
742
+ // Inside a sequence: check global vars + local sequence vars + params
743
+ const localVars = getSequenceLocalVariables(containingSequence.content, containingSequence.parameters);
744
+ isValid = varName in globalVariables || localVars.has(varName);
745
+ }
746
+ else {
747
+ // Outside sequences: only global variables are available
748
+ isValid = varName in globalVariables;
749
+ }
750
+ if (!isValid) {
751
+ const endPos = document.positionAt(match.index + match[0].length);
752
+ const range = new vscode.Range(startPos, endPos);
753
+ const diagnostic = new vscode.Diagnostic(range, `Undefined variable: '${varName}'`, vscode.DiagnosticSeverity.Error);
754
+ diagnostic.source = 'Norn';
755
+ diagnostic.code = 'undefined-variable';
756
+ diagnostics.push(diagnostic);
757
+ }
758
+ }
759
+ // Check for bare text in print statements that isn't a string, variable, or keyword
760
+ const printCommandRegex = /^\s*print\s+(.+)$/i;
761
+ const validKeywords = new Set(['true', 'false', 'null']);
762
+ for (let i = 0; i < lines.length; i++) {
763
+ const line = lines[i];
764
+ // Skip comment lines
765
+ if (isCommentLine(line)) {
766
+ continue;
767
+ }
768
+ // Strip inline comments before processing print statement
769
+ const lineWithoutComment = stripInlineComment(line);
770
+ const printMatch = lineWithoutComment.match(printCommandRegex);
771
+ if (!printMatch) {
772
+ continue;
773
+ }
774
+ const printContent = printMatch[1];
775
+ const printStartCol = line.indexOf(printContent);
776
+ // Check if this print is inside a sequence
777
+ const containingSeq = sequences.find(seq => i > seq.startLine && i < seq.endLine);
778
+ // Get available variables for this scope
779
+ let availableVars;
780
+ if (containingSeq) {
781
+ const localVars = getSequenceLocalVariables(containingSeq.content, containingSeq.parameters);
782
+ availableVars = new Set([...Object.keys(globalVariables), ...localVars]);
783
+ }
784
+ else {
785
+ availableVars = new Set(Object.keys(globalVariables));
786
+ }
787
+ // Parse print content into tokens (respecting quoted strings and operators)
788
+ const tokens = tokenizePrintContent(printContent);
789
+ // Check for missing concatenation operators between tokens
790
+ for (let t = 0; t < tokens.length - 1; t++) {
791
+ const currentToken = tokens[t];
792
+ const nextToken = tokens[t + 1];
793
+ // Skip if current token is an operator
794
+ if (/^[+\-*/|&<>=!]+$/.test(currentToken.text)) {
795
+ continue;
796
+ }
797
+ // If next token is not an operator, we're missing concatenation
798
+ if (!/^[+\-*/|&<>=!]+$/.test(nextToken.text)) {
799
+ // Calculate position between the two tokens
800
+ const errorStartCol = printStartCol + currentToken.start + currentToken.text.length;
801
+ const errorEndCol = printStartCol + nextToken.start;
802
+ const range = new vscode.Range(new vscode.Position(i, errorStartCol), new vscode.Position(i, errorEndCol));
803
+ const diagnostic = new vscode.Diagnostic(range, `Missing concatenation operator '+' between '${currentToken.text}' and '${nextToken.text}'`, vscode.DiagnosticSeverity.Error);
804
+ diagnostic.source = 'Norn';
805
+ diagnostic.code = 'missing-concatenation';
806
+ diagnostics.push(diagnostic);
807
+ }
808
+ }
809
+ for (const token of tokens) {
810
+ // Skip operators (+, -, etc.)
811
+ if (/^[+\-*/|&<>=!]+$/.test(token.text)) {
812
+ continue;
813
+ }
814
+ // Skip quoted strings
815
+ if ((token.text.startsWith('"') && token.text.endsWith('"')) ||
816
+ (token.text.startsWith("'") && token.text.endsWith("'"))) {
817
+ continue;
818
+ }
819
+ // Skip numbers
820
+ if (/^-?\d+(\.\d+)?$/.test(token.text)) {
821
+ continue;
822
+ }
823
+ // Skip keywords (true, false, null)
824
+ if (validKeywords.has(token.text.toLowerCase())) {
825
+ continue;
826
+ }
827
+ // Skip variable references {{var}}
828
+ if (/^\{\{.+\}\}$/.test(token.text)) {
829
+ continue;
830
+ }
831
+ // Check if it's a valid variable (with optional path)
832
+ const varPathMatch = token.text.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)$/);
833
+ if (varPathMatch) {
834
+ const refVarName = varPathMatch[1];
835
+ if (availableVars.has(refVarName)) {
836
+ continue;
837
+ }
838
+ }
839
+ // Check if it looks like a plain text word (not a valid identifier pattern)
840
+ // Flag words that look like they should be quoted
841
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(token.text)) {
842
+ // It's a valid identifier but not a defined variable - flag it
843
+ const tokenStartCol = printStartCol + token.start;
844
+ const range = new vscode.Range(new vscode.Position(i, tokenStartCol), new vscode.Position(i, tokenStartCol + token.text.length));
845
+ const diagnostic = new vscode.Diagnostic(range, `Undefined variable: '${token.text}'. Use quotes for string literals: "${token.text}"`, vscode.DiagnosticSeverity.Error);
846
+ diagnostic.source = 'Norn';
847
+ diagnostic.code = 'undefined-print-variable';
848
+ diagnostics.push(diagnostic);
849
+ }
850
+ else if (!/^[a-zA-Z_]/.test(token.text) === false && !varPathMatch) {
851
+ // Some other bare text that doesn't look like anything valid
852
+ const tokenStartCol = printStartCol + token.start;
853
+ const range = new vscode.Range(new vscode.Position(i, tokenStartCol), new vscode.Position(i, tokenStartCol + token.text.length));
854
+ const diagnostic = new vscode.Diagnostic(range, `Invalid expression: '${token.text}'. Use quotes for string literals.`, vscode.DiagnosticSeverity.Error);
855
+ diagnostic.source = 'Norn';
856
+ diagnostic.code = 'invalid-print-expression';
857
+ diagnostics.push(diagnostic);
858
+ }
859
+ }
860
+ }
861
+ this.diagnosticCollection.set(document.uri, diagnostics);
862
+ }
863
+ /**
864
+ * Update diagnostics for .nornapi files
865
+ */
866
+ updateNornapiDiagnostics(document) {
867
+ const diagnostics = [];
868
+ const lines = document.getText().split('\n');
869
+ const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
870
+ let inEndpointsBlock = false;
871
+ for (let i = 0; i < lines.length; i++) {
872
+ const line = lines[i];
873
+ const trimmed = stripInlineComment(line).trim();
874
+ // Skip empty lines/comments
875
+ if (trimmed === '' || trimmed.startsWith('#')) {
876
+ continue;
877
+ }
878
+ // Track endpoints block state
879
+ if (/^endpoints$/i.test(trimmed)) {
880
+ inEndpointsBlock = true;
881
+ continue;
882
+ }
883
+ if (/^end\s+endpoints$/i.test(trimmed)) {
884
+ inEndpointsBlock = false;
885
+ continue;
886
+ }
887
+ if (!inEndpointsBlock) {
888
+ continue;
889
+ }
890
+ // Valid endpoint declaration: Name: METHOD path
891
+ const validEndpoint = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.+)$/i);
892
+ if (validEndpoint) {
893
+ continue;
894
+ }
895
+ const endpointMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/);
896
+ if (!endpointMatch) {
897
+ continue;
898
+ }
899
+ const endpointName = endpointMatch[1];
900
+ const rhs = endpointMatch[2].trim();
901
+ const colonIndex = line.indexOf(':');
902
+ const valueStart = colonIndex >= 0 ? colonIndex + 1 : 0;
903
+ const valueRange = new vscode.Range(new vscode.Position(i, Math.max(0, valueStart)), new vscode.Position(i, line.length));
904
+ // While typing: "Name:" or "Name: G" should not error yet
905
+ if (rhs === '') {
906
+ continue;
907
+ }
908
+ const firstToken = rhs.split(/\s+/)[0];
909
+ const upperFirstToken = firstToken.toUpperCase();
910
+ const isPartialMethod = httpMethods.some(m => m.startsWith(upperFirstToken));
911
+ if (!rhs.includes(' ') && isPartialMethod) {
912
+ continue;
913
+ }
914
+ if (httpMethods.includes(upperFirstToken) && !rhs.includes(' ')) {
915
+ const diagnostic = new vscode.Diagnostic(valueRange, `Endpoint '${endpointName}' is missing a URL/path after method. Use: ${endpointName}: ${upperFirstToken} /path`, vscode.DiagnosticSeverity.Error);
916
+ diagnostic.source = 'Norn';
917
+ diagnostic.code = 'missing-endpoint-path';
918
+ diagnostics.push(diagnostic);
919
+ continue;
920
+ }
921
+ const diagnostic = new vscode.Diagnostic(valueRange, `Endpoint '${endpointName}' is missing HTTP method. Use: ${endpointName}: GET /path`, vscode.DiagnosticSeverity.Error);
922
+ diagnostic.source = 'Norn';
923
+ diagnostic.code = 'missing-endpoint-method';
924
+ diagnostics.push(diagnostic);
925
+ }
926
+ this.diagnosticCollection.set(document.uri, diagnostics);
927
+ }
928
+ /**
929
+ * Update diagnostics for .nornenv files
930
+ */
931
+ updateNornenvDiagnostics(document) {
932
+ const diagnostics = [];
933
+ const text = document.getText();
934
+ const lines = text.split('\n');
935
+ for (let i = 0; i < lines.length; i++) {
936
+ const line = lines[i];
937
+ const trimmed = line.trim();
938
+ // Skip comments and empty lines
939
+ if (trimmed === '' || trimmed.startsWith('#')) {
940
+ continue;
941
+ }
942
+ // Check for invalid "secret var" pattern
943
+ const secretVarMatch = trimmed.match(/^(secret)\s+(var)\s*/i);
944
+ if (secretVarMatch) {
945
+ const varStart = line.indexOf('var', line.indexOf('secret') + 6);
946
+ const range = new vscode.Range(new vscode.Position(i, varStart), new vscode.Position(i, varStart + 3));
947
+ const diagnostic = new vscode.Diagnostic(range, `Invalid syntax: 'var' is not allowed after 'secret'. Use 'secret variableName = value'.`, vscode.DiagnosticSeverity.Error);
948
+ diagnostic.source = 'Norn';
949
+ diagnostic.code = 'invalid-secret-var';
950
+ diagnostics.push(diagnostic);
951
+ continue;
952
+ }
953
+ // Check for valid patterns - if none match, it's an error
954
+ const validPatterns = [
955
+ /^\[env:[a-zA-Z_][a-zA-Z0-9_-]*\]$/, // Environment section
956
+ /^(secret|var)\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*.+$/, // Complete declaration
957
+ /^(secret|var)\s*$/, // Just keyword (while typing)
958
+ /^(secret|var)\s+[a-zA-Z_][a-zA-Z0-9_]*\s*$/, // Keyword + name (while typing)
959
+ /^(secret|var)\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*$/, // Keyword + name + = (while typing)
960
+ ];
961
+ const isValid = validPatterns.some(pattern => pattern.test(trimmed));
962
+ if (!isValid) {
963
+ const range = new vscode.Range(new vscode.Position(i, 0), new vscode.Position(i, line.length));
964
+ const diagnostic = new vscode.Diagnostic(range, `Invalid syntax. Expected: 'var name = value', 'secret name = value', or '[env:name]'.`, vscode.DiagnosticSeverity.Error);
965
+ diagnostic.source = 'Norn';
966
+ diagnostic.code = 'invalid-nornenv-syntax';
967
+ diagnostics.push(diagnostic);
968
+ }
969
+ }
970
+ this.diagnosticCollection.set(document.uri, diagnostics);
971
+ }
972
+ clearDiagnostics(document) {
973
+ this.diagnosticCollection.delete(document.uri);
974
+ }
975
+ dispose() {
976
+ this.diagnosticCollection.dispose();
977
+ }
978
+ }
979
+ exports.DiagnosticProvider = DiagnosticProvider;
980
+ //# sourceMappingURL=diagnosticProvider.js.map