@tsuzuri-lab/sed-language-server 0.1.2 → 0.3.0

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.
@@ -1,1865 +1,119 @@
1
1
  import { DiagnosticSeverity } from "vscode-languageserver/node";
2
2
  import {
3
- characterAt,
4
- commandSpecificationsFor,
5
- createSubstituteFlagState,
6
- findInvalidReplacementControlEscape,
7
- findLineEnd,
8
- findReplacementDelimiter,
9
- findTransliterateDelimiter,
10
- hasUnescapedTrailingBackslash,
11
- iterateRegexpTokens,
12
- scanAddressSyntax,
13
- scanCommandEndSyntax,
14
- scanCommandRecoverySyntax,
15
- scanFilenameArgumentSyntax,
16
- scanLabelArgumentSyntax,
17
- scanOptionalNumericArgumentSyntax,
18
- scanRegexDelimiter,
19
- scanShellCommandSyntax,
20
- scanSubstituteFlagTokenSyntax,
21
- scanTextCommandSyntax,
22
- scanVersionArgumentSyntax,
23
- skipBlanks,
24
- syntaxPolicyFor,
25
- } from "./sed-syntax.js";
26
- import {
27
- defaultSyntaxProfile,
28
- requireSyntaxProfile,
29
- } from "./syntax-profile.js";
30
-
31
- const SOURCE = "sed-language-server";
32
- const DIALECT_NAME = "\0sed-dialect\0";
3
+ collectSyntaxIssueNodes,
4
+ rangeForNode,
5
+ syntaxTreeFor,
6
+ } from "./syntax.js";
33
7
 
34
- const messages = {
35
- missingDelimiter: `Expected a delimiter after the ${DIALECT_NAME} sed \`s\` command.`,
36
- invalidDelimiter: `A ${DIALECT_NAME} sed \`s\` command cannot use a backslash or newline as its delimiter.`,
37
- unterminatedPattern: `The ${DIALECT_NAME} sed substitute pattern is not terminated.`,
38
- unterminatedReplacement: `The ${DIALECT_NAME} sed substitute replacement is not terminated.`,
39
- invalidFlag: (flag) =>
40
- `Invalid ${DIALECT_NAME} sed substitute flag: \`${flag}\`.`,
41
- repeatedOccurrence:
42
- "A GNU sed `s` command accepts only one occurrence-number option.",
43
- zeroOccurrence: "A GNU sed `s` command occurrence number must not be zero.",
44
- repeatedSubstituteFlag: (flag) =>
45
- `The GNU sed \`${flag}\` substitute flag may only be specified once.`,
46
- missingWriteFileSeparator: `Expected a blank between the ${DIALECT_NAME} sed \`w\` flag and its filename.`,
47
- missingWriteFile: `Expected a filename after the ${DIALECT_NAME} sed substitute \`w\` flag.`,
48
- missingFileSeparator: (command) =>
49
- `Expected a blank between the ${DIALECT_NAME} sed \`${command}\` command and its filename.`,
50
- missingFile: (command) =>
51
- `Expected a filename after the ${DIALECT_NAME} sed \`${command}\` command.`,
52
- missingLabel: `Expected a label after the ${DIALECT_NAME} sed \`:\` command.`,
53
- missingLabelSeparator: (command) =>
54
- `Expected a space between the ${DIALECT_NAME} sed \`${command}\` command and its label.`,
55
- unexpectedClosingBrace: `This ${DIALECT_NAME} sed closing brace has no matching opening brace.`,
56
- missingClosingBraceSeparator: `Expected a newline or semicolon before this ${DIALECT_NAME} sed closing brace.`,
57
- unclosedOpeningBrace: `This ${DIALECT_NAME} sed opening brace is not closed.`,
58
- missingTransliterateDelimiter: `Expected a delimiter after the ${DIALECT_NAME} sed \`y\` command.`,
59
- invalidTransliterateDelimiter: `A ${DIALECT_NAME} sed \`y\` command cannot use a backslash or newline as its delimiter.`,
60
- unterminatedFirstTransliterateString: `The first string in this ${DIALECT_NAME} sed \`y\` command is not terminated.`,
61
- unterminatedSecondTransliterateString: `The second string in this ${DIALECT_NAME} sed \`y\` command is not terminated.`,
62
- missingContextAddressDelimiter: `Expected a delimiter after the backslash in this ${DIALECT_NAME} sed context address.`,
63
- invalidContextAddressDelimiter: `A ${DIALECT_NAME} sed context address cannot use a backslash or newline as its delimiter.`,
64
- unterminatedContextAddress: `This ${DIALECT_NAME} sed context address is not terminated.`,
65
- relativeAddressFirst:
66
- "A GNU sed `+N` or `~N` address can only be the second address in a range.",
67
- missingRangeAddress:
68
- "Expected an address after this comma in a GNU sed address range.",
69
- emptyRegexpModifiers:
70
- "GNU sed regexp modifiers cannot be used with an empty regexp.",
71
- emptySubstituteRegexpModifiers:
72
- "GNU sed substitute regexp modifiers cannot be used with an empty regexp.",
73
- invalidZeroAddress: "GNU sed line address 0 is not valid in this context.",
74
- invalidRegexpBackReference: (regexpMode, number) =>
75
- `The ${DIALECT_NAME} ${regexpMode.toUpperCase()} back-reference \`\\${number}\` does not refer to a preceding subexpression.`,
76
- invalidControlEscape:
77
- "GNU sed does not allow recursive escaping after `\\c`.",
78
- unclosedRegexpBracket: `The ${DIALECT_NAME} sed regular expression has an unmatched \`[\`.`,
79
- missingCommand: `Expected a ${DIALECT_NAME} sed command.`,
80
- unknownCommand: (command) =>
81
- `Unknown ${DIALECT_NAME} sed command: \`${command}\`.`,
82
- unexpectedCommandText: (command) =>
83
- `Unexpected text after the ${DIALECT_NAME} sed \`${command}\` command.`,
84
- missingTextBackslash: (command) =>
85
- `Expected a backslash immediately after the ${DIALECT_NAME} sed \`${command}\` command.`,
86
- unexpectedTextAfterTextBackslash: (command) =>
87
- `Unexpected text after the backslash in the ${DIALECT_NAME} sed \`${command}\` command.`,
88
- missingTextNewline: (command) =>
89
- `Expected a newline after the backslash in the ${DIALECT_NAME} sed \`${command}\` command.`,
90
- missingTextLine: (command) =>
91
- `Expected a line of text for the ${DIALECT_NAME} sed \`${command}\` command.`,
92
- missingTextArgument: (command) =>
93
- `Expected text or a backslash after the ${DIALECT_NAME} sed \`${command}\` command.`,
94
- unsupportedVersion: (requiredVersion) =>
95
- `The GNU sed 4.10 target does not satisfy the required version \`${requiredVersion}\`.`,
96
- tooManyAddressesBeforeCommand: `A ${DIALECT_NAME} sed command accepts at most two addresses.`,
97
- emptyCommandHasAddresses: `An empty ${DIALECT_NAME} sed command does not accept addresses.`,
98
- tooManyAddresses: (command, maximum) => {
99
- if (maximum === 0) {
100
- return `The ${DIALECT_NAME} sed \`${command}\` command does not accept addresses.`;
101
- }
8
+ const diagnosticSource = "sed-language-server";
102
9
 
103
- const count = maximum === 1 ? "one address" : "two addresses";
104
- return `The ${DIALECT_NAME} sed \`${command}\` command accepts at most ${count}.`;
10
+ const issueDescriptions = {
11
+ incomplete_escape: {
12
+ code: "incomplete-escape",
13
+ message: "This escape sequence is incomplete.",
14
+ },
15
+ incomplete_regex: {
16
+ code: "unterminated-regular-expression",
17
+ message: "This regular expression is not terminated.",
18
+ },
19
+ incomplete_replacement: {
20
+ code: "unterminated-replacement",
21
+ message: "This replacement is not terminated.",
105
22
  },
23
+ incomplete_translate: {
24
+ code: "unterminated-translation",
25
+ message: "This translation is not terminated.",
26
+ },
27
+ invalid_control_escape: {
28
+ code: "invalid-control-escape",
29
+ message: "This GNU sed control escape is invalid.",
30
+ },
31
+ unclosed_bracket: {
32
+ code: "unclosed-bracket-expression",
33
+ message: "This bracket expression is not terminated.",
34
+ },
35
+ invalid_command: (node, dialect) => ({
36
+ code: "invalid-command",
37
+ message: `Unknown ${dialectName(dialect)} sed command: \`${node.text}\`.`,
38
+ }),
39
+ invalid_flag: (node, dialect) => ({
40
+ code: "invalid-substitution-flag",
41
+ message: `Unknown ${dialectName(dialect)} sed substitution flag: \`${node.text}\`.`,
42
+ }),
43
+ unexpected_text: (_node, dialect) => ({
44
+ code: "unexpected-text",
45
+ message: `Unexpected text after this ${dialectName(dialect)} sed command.`,
46
+ }),
106
47
  };
107
48
 
108
- function renderMessage(message, syntaxProfile) {
109
- const dialectName = syntaxProfile.dialect === "gnu" ? "GNU" : "POSIX";
110
- return message.replaceAll(DIALECT_NAME, dialectName);
49
+ function dialectName(dialect) {
50
+ return dialect === "gnu" ? "GNU" : "POSIX";
111
51
  }
112
52
 
113
- function findRegexpProblems(text, offset, end, delimiter, syntaxProfile) {
114
- if (syntaxProfile.dialect === "posix" && syntaxProfile.regexpMode === "ere") {
115
- return [];
116
- }
117
-
118
- let nextSubexpression = 1;
119
- let subexpressionDepth = 0;
120
- const trackedSubexpressionsByDepth = new Map();
121
- const closedSubexpressions = new Set();
122
- const problems = [];
123
-
124
- for (const token of iterateRegexpTokens(
125
- text,
126
- offset,
127
- end,
128
- delimiter,
129
- syntaxProfile,
130
- )) {
131
- if (token.kind === "invalid-control-escape") {
132
- problems.push(
133
- issue(
134
- "regexp-invalid-control-escape",
135
- messages.invalidControlEscape,
136
- token.offset,
137
- token.endOffset,
138
- ),
139
- );
140
- } else if (token.kind === "unclosed-bracket-expression") {
141
- problems.push(
142
- issue(
143
- "regexp-unclosed-bracket-expression",
144
- messages.unclosedRegexpBracket,
145
- token.offset,
146
- token.endOffset,
147
- ),
148
- );
149
- } else if (token.kind === "subexpression-open") {
150
- subexpressionDepth += 1;
151
- if (nextSubexpression <= 9) {
152
- trackedSubexpressionsByDepth.set(subexpressionDepth, nextSubexpression);
153
- }
154
- nextSubexpression += 1;
155
- } else if (token.kind === "subexpression-close") {
156
- const subexpression =
157
- trackedSubexpressionsByDepth.get(subexpressionDepth);
158
- if (subexpression !== undefined) {
159
- closedSubexpressions.add(subexpression);
160
- trackedSubexpressionsByDepth.delete(subexpressionDepth);
161
- }
162
- subexpressionDepth = Math.max(0, subexpressionDepth - 1);
163
- } else if (
164
- token.kind === "back-reference" &&
165
- !closedSubexpressions.has(Number(token.value))
166
- ) {
167
- problems.push(
168
- issue(
169
- `${syntaxProfile.regexpMode}-invalid-back-reference`,
170
- messages.invalidRegexpBackReference(
171
- syntaxProfile.regexpMode,
172
- token.value,
173
- ),
174
- token.offset,
175
- token.endOffset,
176
- ),
177
- );
178
- }
53
+ function descriptionFor(node, dialect) {
54
+ const description = issueDescriptions[node.type];
55
+ if (typeof description === "function") {
56
+ return description(node, dialect);
179
57
  }
180
-
181
- return problems;
182
- }
183
-
184
- const commandDefinitionsByProfile = new Map();
185
-
186
- function commandDefinitionsFor(syntaxProfile) {
187
- let definitions = commandDefinitionsByProfile.get(syntaxProfile);
188
- if (definitions !== undefined) {
189
- return definitions;
58
+ if (description !== undefined) {
59
+ return description;
190
60
  }
191
61
 
192
- definitions = new Map(
193
- commandSpecificationsFor(syntaxProfile).map(
194
- ({ command, kind, maximumAddresses }) => [
195
- command,
196
- { kind, maximumAddresses },
197
- ],
198
- ),
199
- );
200
- commandDefinitionsByProfile.set(syntaxProfile, definitions);
201
- return definitions;
202
- }
203
-
204
- function scanAddress(text, offset, end, syntaxProfile) {
205
- const result = scanAddressSyntax(text, offset, end, syntaxProfile);
206
- if (result.kind === "none") {
207
- return result;
208
- }
209
-
210
- if (result.kind === "indeterminate") {
62
+ if (node.isMissing) {
211
63
  return {
212
- endOffset: result.endOffset ?? end,
213
- kind: "invalid",
214
- problem: null,
64
+ code: "missing-syntax",
65
+ message: `Expected ${node.type.replaceAll("_", " ")}.`,
215
66
  };
216
67
  }
217
68
 
218
- if (result.kind === "invalid") {
219
- if (result.reason === "missing-delimiter") {
220
- return {
221
- endOffset: result.endOffset ?? result.delimiterOffset ?? end,
222
- kind: "invalid",
223
- problem: issue(
224
- "address-missing-delimiter",
225
- messages.missingContextAddressDelimiter,
226
- offset,
227
- offset + 1,
228
- ),
229
- };
230
- }
231
-
232
- if (result.reason === "invalid-delimiter") {
233
- return {
234
- endOffset: result.endOffset ?? result.delimiterOffset ?? end,
235
- kind: "invalid",
236
- problem: issue(
237
- "address-invalid-delimiter",
238
- messages.invalidContextAddressDelimiter,
239
- result.delimiterOffset,
240
- result.delimiterOffset + (result.delimiter?.width ?? 1),
241
- ),
242
- };
243
- }
244
-
245
- return {
246
- endOffset: result.endOffset ?? result.delimiterOffset ?? end,
247
- kind: "invalid",
248
- problem: issue(
249
- "address-unterminated-context",
250
- messages.unterminatedContextAddress,
251
- offset,
252
- result.endOffset ?? end,
253
- ),
254
- };
255
- }
256
-
257
- const addressProblems = [];
258
-
259
- if (
260
- result.addressKind === "regular-expression" &&
261
- result.value === "" &&
262
- result.modifiers.length > 0
263
- ) {
264
- addressProblems.push(
265
- issue(
266
- "address-empty-regexp-modifiers",
267
- messages.emptyRegexpModifiers,
268
- result.modifiers[0].startOffset,
269
- result.modifiers.at(-1).endOffset,
270
- ),
271
- );
272
- }
273
-
274
69
  return {
275
- kind: "valid",
276
- addressProblems,
277
- addressKind: result.addressKind,
278
- endOffset: result.endOffset,
279
- modifiers: result.modifiers,
280
- startOffset: result.startOffset,
281
- value: result.value,
282
- regexpProblems:
283
- result.addressKind !== "regular-expression" || result.isUndefined
284
- ? []
285
- : findRegexpProblems(
286
- text,
287
- result.patternStartOffset,
288
- result.patternEndOffset,
289
- result.delimiter,
290
- syntaxProfile,
291
- ),
70
+ code: "syntax-error",
71
+ message: `Invalid ${dialectName(dialect)} sed syntax.`,
292
72
  };
293
73
  }
294
-
295
- function isZeroDecimal(value) {
296
- if (value === "") {
297
- return false;
298
- }
299
-
300
- for (const digit of value) {
301
- if (digit !== "0") {
302
- return false;
303
- }
304
- }
305
- return true;
306
- }
307
-
308
- function addressUsageProblems(
309
- addresses,
310
- command,
311
- hasOmittedAddress,
312
- negationOffset,
313
- syntaxProfile,
314
- ) {
315
- if (syntaxProfile.dialect !== "gnu") {
316
- return [];
74
+ function issuePriority(node) {
75
+ if (node.isError) {
76
+ return 0;
317
77
  }
318
-
319
- const problems = [];
320
- const firstAddress = addresses[0];
321
- // GNU sed represents an omitted or zero relative value as a null address.
322
- // Only a positive relative value is invalid in the first position.
323
- if (
324
- firstAddress !== undefined &&
325
- (firstAddress.addressKind === "relative-line-count" ||
326
- firstAddress.addressKind === "relative-line-multiple") &&
327
- firstAddress.value !== null &&
328
- !isZeroDecimal(firstAddress.value)
329
- ) {
330
- problems.push(
331
- issue(
332
- "address-relative-first",
333
- messages.relativeAddressFirst,
334
- firstAddress.startOffset,
335
- firstAddress.endOffset,
336
- ),
337
- );
338
- }
339
-
340
- if (firstAddress === undefined) {
341
- return problems;
342
- }
343
-
344
- const firstLineNumber =
345
- firstAddress.addressKind === "line-number"
346
- ? firstAddress.value
347
- : firstAddress.addressKind === "line-number-step"
348
- ? firstAddress.value.first
349
- : null;
350
- if (firstLineNumber === null || !isZeroDecimal(firstLineNumber)) {
351
- return problems;
78
+ if (node.isMissing) {
79
+ return 1;
352
80
  }
353
-
354
- const hasPositiveStep =
355
- firstAddress.addressKind === "line-number-step" &&
356
- firstAddress.value.step !== null &&
357
- !isZeroDecimal(firstAddress.value.step);
358
- const isZeroRegexpRange = addresses[1]?.addressKind === "regular-expression";
359
- const isStandaloneRead =
360
- addresses.length === 1 &&
361
- !hasOmittedAddress &&
362
- negationOffset === null &&
363
- command === "r";
364
- if (!hasPositiveStep && !isZeroRegexpRange && !isStandaloneRead) {
365
- problems.push(
366
- issue(
367
- "address-zero-invalid",
368
- messages.invalidZeroAddress,
369
- firstAddress.startOffset,
370
- firstAddress.endOffset,
371
- ),
372
- );
373
- }
374
-
375
- return problems;
81
+ return 2;
376
82
  }
377
83
 
378
- function findCommand(text, lineStart, lineEnd, syntaxProfile) {
379
- const policy = syntaxPolicyFor(syntaxProfile);
380
- const addressScanEnd = policy.regexp.escapedPhysicalNewlines
381
- ? text.length
382
- : lineEnd;
383
- let cursor = skipBlanks(text, lineStart, lineEnd);
384
- let problem = null;
385
- let negationOffset = null;
386
- let hasOmittedAddress = false;
387
- const addresses = [];
388
- const addressProblems = [];
389
- const regexpProblems = [];
390
-
391
- if (text[cursor] === ",") {
392
- if (syntaxProfile.dialect === "gnu") {
393
- problem = issue(
394
- "address-range-missing",
395
- messages.missingRangeAddress,
396
- cursor,
397
- cursor + 1,
398
- );
399
- } else {
400
- hasOmittedAddress = true;
401
- }
402
- cursor = skipBlanks(text, cursor + 1, lineEnd);
403
- }
404
-
405
- const firstAddress = scanAddress(text, cursor, addressScanEnd, syntaxProfile);
406
-
407
- if (firstAddress.kind === "invalid") {
408
- if (firstAddress.endOffset > lineEnd) {
409
- lineEnd = findLineEnd(text, firstAddress.endOffset);
410
- }
411
- return {
412
- addressProblems,
413
- addresses,
414
- regexpProblems,
415
- commandOffset: null,
416
- hasOmittedAddress: hasOmittedAddress || firstAddress.problem === null,
417
- lineEnd,
418
- negationOffset,
419
- problem: hasOmittedAddress ? null : firstAddress.problem,
420
- resumeOffset: null,
421
- };
422
- }
423
-
424
- if (firstAddress.kind === "valid") {
425
- addressProblems.push(...firstAddress.addressProblems);
426
- regexpProblems.push(...(firstAddress.regexpProblems ?? []));
427
- addresses.push({
428
- addressKind: firstAddress.addressKind,
429
- endOffset: firstAddress.endOffset,
430
- modifiers: firstAddress.modifiers,
431
- startOffset: firstAddress.startOffset,
432
- value: firstAddress.value,
433
- });
434
- if (firstAddress.endOffset > lineEnd) {
435
- lineEnd = findLineEnd(text, firstAddress.endOffset);
436
- }
437
- cursor = skipBlanks(text, firstAddress.endOffset, lineEnd);
438
-
439
- while (text[cursor] === ",") {
440
- const commaOffset = cursor;
441
- cursor = skipBlanks(text, commaOffset + 1, lineEnd);
442
- const nextAddress = scanAddress(
443
- text,
444
- cursor,
445
- addressScanEnd,
446
- syntaxProfile,
447
- );
448
-
449
- if (nextAddress.kind === "invalid") {
450
- if (nextAddress.endOffset > lineEnd) {
451
- lineEnd = findLineEnd(text, nextAddress.endOffset);
452
- }
453
- if (addresses.length >= 2) {
454
- return {
455
- addressProblems,
456
- addresses,
457
- regexpProblems,
458
- commandOffset: null,
459
- hasOmittedAddress,
460
- lineEnd,
461
- negationOffset,
462
- problem: issue(
463
- "address-too-many",
464
- messages.tooManyAddressesBeforeCommand,
465
- commaOffset,
466
- commaOffset + 1,
467
- ),
468
- resumeOffset: null,
469
- };
470
- }
471
-
472
- return {
473
- addressProblems,
474
- addresses,
475
- regexpProblems,
476
- commandOffset: null,
477
- hasOmittedAddress: hasOmittedAddress || nextAddress.problem === null,
478
- lineEnd,
479
- negationOffset,
480
- problem: nextAddress.problem,
481
- resumeOffset: null,
482
- };
483
- }
484
-
485
- if (nextAddress.kind === "none") {
486
- if (addresses.length >= 2) {
487
- problem = issue(
488
- "address-too-many",
489
- messages.tooManyAddressesBeforeCommand,
490
- commaOffset,
491
- commaOffset + 1,
492
- );
493
- } else if (syntaxProfile.dialect === "gnu") {
494
- problem = issue(
495
- "address-range-missing",
496
- messages.missingRangeAddress,
497
- commaOffset,
498
- commaOffset + 1,
499
- );
500
- } else {
501
- hasOmittedAddress = true;
502
- }
503
-
504
- if (text[cursor] === ";") {
505
- return {
506
- addressProblems,
507
- addresses,
508
- regexpProblems,
509
- commandOffset: null,
510
- hasOmittedAddress,
511
- lineEnd,
512
- negationOffset,
513
- problem,
514
- resumeOffset: skipEmptyCommands(text, cursor, lineEnd),
515
- };
516
- }
517
-
518
- break;
519
- }
520
-
521
- addressProblems.push(...nextAddress.addressProblems);
522
- addresses.push({
523
- addressKind: nextAddress.addressKind,
524
- endOffset: nextAddress.endOffset,
525
- modifiers: nextAddress.modifiers,
526
- startOffset: nextAddress.startOffset,
527
- value: nextAddress.value,
528
- });
529
- if (nextAddress.endOffset > lineEnd) {
530
- lineEnd = findLineEnd(text, nextAddress.endOffset);
531
- }
532
- regexpProblems.push(...(nextAddress.regexpProblems ?? []));
533
- cursor = skipBlanks(text, nextAddress.endOffset, lineEnd);
534
- }
535
- }
536
-
537
- if (text[cursor] === "!") {
538
- negationOffset = cursor;
539
- cursor = skipBlanks(text, cursor + 1, lineEnd);
540
- }
541
-
542
- const commandOffset = cursor < lineEnd ? cursor : null;
543
- if (problem === null) {
544
- addressProblems.push(
545
- ...addressUsageProblems(
546
- addresses,
547
- commandOffset === null ? null : text[commandOffset],
548
- hasOmittedAddress,
549
- negationOffset,
550
- syntaxProfile,
551
- ),
552
- );
553
- }
554
-
555
- return {
556
- addressProblems,
557
- addresses,
558
- regexpProblems,
559
- commandOffset,
560
- hasOmittedAddress,
561
- lineEnd,
562
- negationOffset,
563
- problem,
564
- resumeOffset: null,
565
- };
566
- }
567
-
568
- function diagnoseTextCommand(
569
- text,
570
- commandOffset,
571
- lineEnd,
572
- hasPhysicalNewline,
573
- syntaxProfile,
574
- ) {
575
- const command = text[commandOffset];
576
- const result = scanTextCommandSyntax(
577
- text,
578
- commandOffset,
579
- lineEnd,
580
- hasPhysicalNewline,
581
- syntaxProfile,
582
- );
583
- let problem = null;
584
-
585
- if (result.kind === "invalid") {
586
- if (result.reason === "missing-backslash") {
587
- problem = issue(
588
- "text-missing-backslash",
589
- messages.missingTextBackslash(command),
590
- result.startOffset,
591
- result.endOffset,
592
- );
593
- } else if (result.reason === "unexpected-after-backslash") {
594
- problem = issue(
595
- "text-unexpected-after-backslash",
596
- messages.unexpectedTextAfterTextBackslash(command),
597
- result.startOffset,
598
- result.endOffset,
599
- );
600
- } else if (result.reason === "missing-newline") {
601
- problem = issue(
602
- "text-missing-newline",
603
- messages.missingTextNewline(command),
604
- result.startOffset,
605
- result.endOffset,
606
- );
607
- } else {
608
- problem = issue(
609
- "text-missing-argument",
610
- messages.missingTextArgument(command),
611
- result.startOffset,
612
- result.endOffset,
613
- );
614
- }
615
- }
616
-
617
- if (!result.consumesFollowingTextLine) {
618
- return { problem, textArgument: null };
619
- }
620
-
621
- return {
622
- problem,
623
- textArgument: {
624
- allowsMissingLine: result.allowsMissingFollowingTextLine ?? false,
625
- command,
626
- expectedLineOffset:
627
- result.backslashOffset ?? Math.max(commandOffset, lineEnd - 1),
628
- },
629
- };
630
- }
631
-
632
- function scanShellCommand(
633
- text,
634
- commandOffset,
635
- lineEnd,
636
- hasPhysicalNewline,
637
- syntaxProfile,
638
- ) {
639
- const result = scanShellCommandSyntax(
640
- text,
641
- commandOffset,
642
- lineEnd,
643
- hasPhysicalNewline,
644
- syntaxProfile,
645
- );
646
- if (!result.consumesFollowingTextLine) {
647
- return null;
648
- }
649
-
650
- return {
651
- allowsMissingLine: true,
652
- command: text[commandOffset],
653
- expectedLineOffset: Math.max(commandOffset, lineEnd - 1),
654
- };
655
- }
656
-
657
- function diagnoseFileCommand(text, commandOffset, lineEnd, syntaxProfile) {
658
- const command = text[commandOffset];
659
- const argument = scanFilenameArgumentSyntax(
660
- text,
661
- commandOffset,
662
- lineEnd,
663
- syntaxProfile,
664
- );
665
- const problemKind = !argument.hasName
666
- ? "missing-name"
667
- : argument.hasValidSeparator
668
- ? null
669
- : "missing-separator";
670
- if (problemKind === null) {
671
- return null;
672
- }
673
-
674
- const codePrefix = command.toLowerCase() === "r" ? "read-file" : "write-file";
675
- return issue(
676
- `${codePrefix}-${problemKind}`,
677
- problemKind === "missing-name"
678
- ? messages.missingFile(command)
679
- : messages.missingFileSeparator(command),
680
- commandOffset,
681
- commandOffset + 1,
682
- );
683
- }
684
-
685
- function diagnoseLabelCommand(
686
- text,
687
- commandOffset,
688
- lineEnd,
689
- insideBlock,
690
- syntaxProfile,
691
- ) {
692
- const command = text[commandOffset];
693
- const policy = syntaxPolicyFor(syntaxProfile);
694
- const argument = scanLabelArgumentSyntax(
695
- text,
696
- commandOffset,
697
- lineEnd,
698
- syntaxProfile,
699
- );
700
- let problem = null;
701
-
702
- if (!argument.hasLabel) {
703
- if (command === ":") {
704
- problem = issue(
705
- "label-missing",
706
- messages.missingLabel,
707
- commandOffset,
708
- commandOffset + 1,
709
- );
710
- } else if (
711
- insideBlock &&
712
- policy.commandEnd.rejectsTrailingBlanksInBlock &&
713
- argument.fieldStartOffset < lineEnd
714
- ) {
715
- problem = issue(
716
- "command-unexpected-text",
717
- messages.unexpectedCommandText(command),
718
- argument.fieldStartOffset,
719
- lineEnd,
720
- );
721
- }
722
- } else if (command !== ":" && !argument.hasValidSeparator) {
723
- const codePrefix = command === "b" ? "branch-label" : "test-label";
724
- problem = issue(
725
- `${codePrefix}-missing-separator`,
726
- messages.missingLabelSeparator(command),
727
- commandOffset,
728
- commandOffset + 1,
729
- );
730
- }
731
-
732
- return {
733
- problem,
734
- nextCommandBoundary: argument.nextCommandBoundary,
735
- nextCommandOffset: argument.nextCommandOffset,
736
- };
737
- }
738
-
739
- function diagnoseVersionCommand(text, commandOffset, lineEnd, syntaxProfile) {
740
- const argument = scanVersionArgumentSyntax(
741
- text,
742
- commandOffset,
743
- lineEnd,
744
- syntaxProfile,
745
- );
746
-
747
- return {
748
- problem: argument.isSupported
749
- ? null
750
- : issue(
751
- "version-requires-newer-sed",
752
- messages.unsupportedVersion(argument.comparedVersion),
753
- argument.startOffset,
754
- argument.endOffset,
755
- ),
756
- nextCommandBoundary: argument.nextCommandBoundary,
757
- nextCommandOffset: argument.nextCommandOffset,
758
- };
759
- }
760
-
761
- function scanEmptyCommands(text, offset, lineEnd, boundary = "direct") {
762
- let cursor = offset;
763
- let nextBoundary = boundary;
764
-
765
- while (cursor < lineEnd) {
766
- cursor = skipBlanks(text, cursor, lineEnd);
767
- if (text[cursor] !== ";") {
768
- break;
769
- }
770
- nextBoundary = "separated";
771
- cursor += 1;
772
- }
773
-
774
- return {
775
- boundary: nextBoundary,
776
- offset: skipBlanks(text, cursor, lineEnd),
777
- };
778
- }
779
-
780
- function skipEmptyCommands(text, offset, lineEnd) {
781
- return scanEmptyCommands(text, offset, lineEnd).offset;
782
- }
783
-
784
- function diagnoseCommandEnd(
785
- text,
786
- command,
787
- commandEnd,
788
- lineEnd,
789
- syntaxProfile,
790
- state = {},
791
- ) {
792
- const result = scanCommandEndSyntax(
793
- text,
794
- commandEnd,
795
- lineEnd,
796
- syntaxProfile,
797
- state,
798
- );
799
- return {
800
- problem:
801
- result.kind === "unexpected"
802
- ? issue(
803
- "command-unexpected-text",
804
- messages.unexpectedCommandText(command),
805
- result.startOffset,
806
- result.endOffset,
807
- )
808
- : null,
809
- nextCommandOffset: result.nextCommandOffset,
810
- nextCommandBoundary: result.nextCommandBoundary,
811
- };
812
- }
813
-
814
- function simpleCommandEnd(
815
- text,
816
- command,
817
- commandOffset,
818
- lineEnd,
819
- syntaxProfile,
820
- state = {},
821
- ) {
822
- return diagnoseCommandEnd(
823
- text,
824
- command,
825
- commandOffset + 1,
826
- lineEnd,
827
- syntaxProfile,
828
- state,
829
- );
830
- }
831
-
832
- function diagnoseTransliterate(
833
- text,
834
- commandOffset,
835
- lineEnd,
836
- recoverAtClosingBrace,
837
- syntaxProfile,
838
- ) {
839
- const delimiterOffset = commandOffset + 1;
840
- if (delimiterOffset >= lineEnd) {
841
- return {
842
- problem: issue(
843
- "transliterate-missing-delimiter",
844
- messages.missingTransliterateDelimiter,
845
- commandOffset,
846
- commandOffset + 1,
847
- ),
848
- nextCommandOffset: null,
849
- };
850
- }
851
-
852
- const delimiter = characterAt(text, delimiterOffset);
853
- if (
854
- delimiter === null ||
855
- delimiter.value === "\\" ||
856
- delimiter.value === "\n"
857
- ) {
858
- return {
859
- problem: issue(
860
- "transliterate-invalid-delimiter",
861
- messages.invalidTransliterateDelimiter,
862
- delimiterOffset,
863
- delimiterOffset + (delimiter?.width ?? 1),
864
- ),
865
- nextCommandOffset: null,
866
- };
867
- }
868
-
869
- const firstStringEnd = findTransliterateDelimiter(
870
- text,
871
- delimiterOffset + delimiter.width,
872
- lineEnd,
873
- delimiter,
874
- syntaxProfile,
875
- );
876
- if (firstStringEnd === null) {
877
- return {
878
- problem: issue(
879
- "transliterate-unterminated-first-string",
880
- messages.unterminatedFirstTransliterateString,
881
- commandOffset,
882
- lineEnd,
883
- ),
884
- nextCommandOffset: null,
885
- };
886
- }
887
-
888
- const secondStringEnd = findTransliterateDelimiter(
889
- text,
890
- firstStringEnd + delimiter.width,
891
- lineEnd,
892
- delimiter,
893
- syntaxProfile,
894
- );
895
- if (secondStringEnd === null) {
896
- return {
897
- problem: issue(
898
- "transliterate-unterminated-second-string",
899
- messages.unterminatedSecondTransliterateString,
900
- commandOffset,
901
- lineEnd,
902
- ),
903
- nextCommandOffset: null,
904
- };
905
- }
906
-
907
- const commandEnd = secondStringEnd + delimiter.width;
908
- return diagnoseCommandEnd(text, "y", commandEnd, lineEnd, syntaxProfile, {
909
- insideBlock: recoverAtClosingBrace,
910
- recoverAtClosingBrace,
911
- });
912
- }
913
-
914
- function scanSubstituteFlags(
915
- text,
916
- offset,
917
- lineEnd,
918
- recoverAtClosingBrace,
919
- syntaxProfile,
920
- ) {
921
- const policy = syntaxPolicyFor(syntaxProfile);
922
- const canRecoverAtClosingBrace =
923
- recoverAtClosingBrace || policy.commandEnd.closingBraceTerminates;
924
- let cursor = offset;
925
- let flagState = createSubstituteFlagState();
926
- const flags = [];
927
- const regexpModifiers = [];
928
-
929
- function result(fields) {
930
- return { flagState, flags, regexpModifiers, ...fields };
931
- }
932
-
933
- while (cursor < lineEnd) {
934
- const character = characterAt(text, cursor);
935
- if (character === null) {
936
- break;
937
- }
938
-
939
- if (character.value === ";") {
940
- return result({
941
- problem: null,
942
- consumedUntil: cursor + character.width,
943
- nextCommandOffset: cursor + character.width,
944
- nextCommandBoundary: "separated",
945
- });
946
- }
947
-
948
- if (policy.commandEnd.commentTerminates && character.value === "#") {
949
- return result({
950
- problem: null,
951
- consumedUntil: lineEnd,
952
- nextCommandOffset: null,
953
- nextCommandBoundary: null,
954
- });
955
- }
956
-
957
- if (canRecoverAtClosingBrace && character.value === "}") {
958
- return result({
959
- problem: null,
960
- consumedUntil: lineEnd,
961
- nextCommandOffset: cursor,
962
- nextCommandBoundary: "direct",
963
- });
964
- }
84
+ function preferSpecificIssues(nodes) {
85
+ const issuesByRange = new Map();
965
86
 
87
+ for (const node of nodes) {
88
+ const key = `${node.startIndex}:${node.endIndex}`;
89
+ const existing = issuesByRange.get(key);
966
90
  if (
967
- policy.substituteFlags.separator === "optional-blanks" &&
968
- (character.value === " " || character.value === "\t")
91
+ existing === undefined ||
92
+ issuePriority(node) > issuePriority(existing)
969
93
  ) {
970
- cursor = skipBlanks(text, cursor, lineEnd);
971
- continue;
94
+ issuesByRange.set(key, node);
972
95
  }
973
-
974
- if (
975
- recoverAtClosingBrace &&
976
- (character.value === " " || character.value === "\t")
977
- ) {
978
- const argumentOffset = skipBlanks(text, cursor, lineEnd);
979
- if (text[argumentOffset] === "}") {
980
- return result({
981
- problem: null,
982
- consumedUntil: lineEnd,
983
- nextCommandOffset: argumentOffset,
984
- nextCommandBoundary: "direct",
985
- });
986
- }
987
-
988
- if (argumentOffset >= lineEnd || text[argumentOffset] === ";") {
989
- return result({
990
- problem: issue(
991
- "command-unexpected-text",
992
- messages.unexpectedCommandText("s"),
993
- cursor,
994
- argumentOffset,
995
- ),
996
- consumedUntil: lineEnd,
997
- nextCommandOffset:
998
- argumentOffset >= lineEnd ? null : argumentOffset + 1,
999
- nextCommandBoundary: argumentOffset >= lineEnd ? null : "separated",
1000
- });
1001
- }
1002
- }
1003
-
1004
- const token = scanSubstituteFlagTokenSyntax(
1005
- text,
1006
- cursor,
1007
- lineEnd,
1008
- flagState,
1009
- syntaxProfile,
1010
- );
1011
- if (token.kind === "flag" || token.kind === "occurrence") {
1012
- flags.push({
1013
- endOffset: token.endOffset,
1014
- kind: token.kind,
1015
- startOffset: token.startOffset,
1016
- value: token.value,
1017
- });
1018
- if (token.kind === "flag" && ["i", "I", "m", "M"].includes(token.value)) {
1019
- regexpModifiers.push({
1020
- endOffset: token.endOffset,
1021
- startOffset: token.startOffset,
1022
- value: token.value,
1023
- });
1024
- }
1025
- flagState = token.state;
1026
- cursor = token.endOffset;
1027
- continue;
1028
- }
1029
-
1030
- if (token.kind === "write") {
1031
- const filename = scanFilenameArgumentSyntax(
1032
- text,
1033
- cursor,
1034
- lineEnd,
1035
- syntaxProfile,
1036
- );
1037
- const filenameProblem = !filename.hasName
1038
- ? "missing-name"
1039
- : filename.hasValidSeparator
1040
- ? null
1041
- : "missing-separator";
1042
- if (filenameProblem !== null) {
1043
- return result({
1044
- problem: issue(
1045
- `substitute-write-file-${filenameProblem}`,
1046
- filenameProblem === "missing-name"
1047
- ? messages.missingWriteFile
1048
- : messages.missingWriteFileSeparator,
1049
- cursor,
1050
- cursor + character.width,
1051
- ),
1052
- consumedUntil: lineEnd,
1053
- nextCommandOffset: null,
1054
- nextCommandBoundary: null,
1055
- });
1056
- }
1057
-
1058
- return result({
1059
- problem: null,
1060
- consumedUntil: lineEnd,
1061
- nextCommandOffset: null,
1062
- nextCommandBoundary: null,
1063
- });
1064
- }
1065
-
1066
- const recovery = scanCommandRecoverySyntax(
1067
- text,
1068
- token.endOffset,
1069
- lineEnd,
1070
- syntaxProfile,
1071
- { recoverAtClosingBrace: canRecoverAtClosingBrace },
1072
- );
1073
- const problem =
1074
- syntaxProfile.dialect === "gnu" && token.reason === "repeated-occurrence"
1075
- ? issue(
1076
- "substitute-occurrence-repeated",
1077
- messages.repeatedOccurrence,
1078
- token.startOffset,
1079
- token.endOffset,
1080
- )
1081
- : syntaxProfile.dialect === "gnu" && token.reason === "zero-occurrence"
1082
- ? issue(
1083
- "substitute-occurrence-zero",
1084
- messages.zeroOccurrence,
1085
- token.startOffset,
1086
- token.endOffset,
1087
- )
1088
- : syntaxProfile.dialect === "gnu" &&
1089
- (token.reason === "repeated-global" ||
1090
- token.reason === "repeated-print")
1091
- ? issue(
1092
- "substitute-flag-repeated",
1093
- messages.repeatedSubstituteFlag(token.value),
1094
- token.startOffset,
1095
- token.endOffset,
1096
- )
1097
- : issue(
1098
- "substitute-invalid-flag",
1099
- messages.invalidFlag(token.value),
1100
- token.startOffset,
1101
- token.endOffset,
1102
- );
1103
- return result({
1104
- problem,
1105
- consumedUntil: lineEnd,
1106
- nextCommandOffset: recovery.nextCommandOffset,
1107
- nextCommandBoundary: recovery.nextCommandBoundary,
1108
- });
1109
96
  }
1110
97
 
1111
- return result({
1112
- problem: null,
1113
- consumedUntil: lineEnd,
1114
- nextCommandOffset: null,
1115
- nextCommandBoundary: null,
1116
- });
1117
- }
1118
-
1119
- function issue(code, message, startOffset, endOffset) {
1120
- return {
1121
- code,
1122
- message,
1123
- startOffset,
1124
- endOffset: Math.max(startOffset + 1, endOffset),
1125
- };
98
+ return [...issuesByRange.values()].sort(
99
+ (left, right) =>
100
+ left.startIndex - right.startIndex || left.endIndex - right.endIndex,
101
+ );
1126
102
  }
1127
103
 
1128
- function diagnoseSubstitute(
1129
- text,
1130
- commandOffset,
1131
- lineEnd,
1132
- recoverAtClosingBrace,
1133
- syntaxProfile,
1134
- ) {
1135
- const policy = syntaxPolicyFor(syntaxProfile);
1136
- const delimiterOffset = commandOffset + 1;
1137
- if (delimiterOffset >= lineEnd) {
1138
- return {
1139
- problem: issue(
1140
- "substitute-missing-delimiter",
1141
- messages.missingDelimiter,
1142
- commandOffset,
1143
- commandOffset + 1,
1144
- ),
1145
- consumedUntil: lineEnd,
1146
- nextCommandOffset: null,
1147
- };
1148
- }
1149
-
1150
- const delimiter = characterAt(text, delimiterOffset);
1151
- if (delimiter === null || delimiter.value === "\\") {
1152
- return {
1153
- problem: issue(
1154
- "substitute-invalid-delimiter",
1155
- messages.invalidDelimiter,
1156
- delimiterOffset,
1157
- delimiterOffset + (delimiter?.width ?? 1),
1158
- ),
1159
- consumedUntil: lineEnd,
1160
- nextCommandOffset: null,
1161
- };
1162
- }
104
+ export function createDiagnostics(document, dialect) {
105
+ const tree = syntaxTreeFor(document, dialect);
1163
106
 
1164
- const pattern = scanRegexDelimiter(
1165
- text,
1166
- delimiterOffset + delimiter.width,
1167
- policy.regexp.escapedPhysicalNewlines ? text.length : lineEnd,
1168
- delimiter,
1169
- syntaxProfile,
1170
- );
1171
- const patternEnd =
1172
- pattern.closingOffset ??
1173
- (syntaxProfile.dialect === "posix" && pattern.hasUnclosedBracketExpression
1174
- ? pattern.possibleClosingOffset
1175
- : null);
1176
- if (patternEnd === null) {
1177
- if (
1178
- syntaxProfile.dialect === "posix" &&
1179
- (pattern.hasUnclosedBracketExpression ||
1180
- pattern.isDelimiterInterpretationUnspecified)
1181
- ) {
107
+ return preferSpecificIssues(collectSyntaxIssueNodes(tree.rootNode)).map(
108
+ (node) => {
109
+ const { code, message } = descriptionFor(node, dialect);
1182
110
  return {
1183
- problem: null,
1184
- consumedUntil: lineEnd,
1185
- nextCommandOffset: null,
111
+ severity: DiagnosticSeverity.Error,
112
+ range: rangeForNode(document, node),
113
+ message,
114
+ code,
115
+ source: diagnosticSource,
1186
116
  };
1187
- }
1188
-
1189
- const failureOffset =
1190
- pattern.failureOffset ??
1191
- (policy.regexp.escapedPhysicalNewlines ? text.length : lineEnd);
1192
- return {
1193
- problem: issue(
1194
- "substitute-unterminated-pattern",
1195
- messages.unterminatedPattern,
1196
- commandOffset,
1197
- failureOffset,
1198
- ),
1199
- consumedUntil: failureOffset,
1200
- nextCommandOffset: null,
1201
- };
1202
- }
1203
-
1204
- const patternProblems =
1205
- pattern.closingOffset === null
1206
- ? []
1207
- : findRegexpProblems(
1208
- text,
1209
- delimiterOffset + delimiter.width,
1210
- patternEnd,
1211
- delimiter,
1212
- syntaxProfile,
1213
- );
1214
- const replacement = findReplacementDelimiter(
1215
- text,
1216
- patternEnd + delimiter.width,
1217
- delimiter,
1218
- syntaxProfile,
1219
- );
1220
- if (replacement.closingOffset === null) {
1221
- return {
1222
- problem: issue(
1223
- "substitute-unterminated-replacement",
1224
- messages.unterminatedReplacement,
1225
- commandOffset,
1226
- replacement.failureOffset,
1227
- ),
1228
- consumedUntil: replacement.failureOffset,
1229
- nextCommandOffset: null,
1230
- };
1231
- }
1232
-
1233
- const invalidReplacementControlEscape = findInvalidReplacementControlEscape(
1234
- text,
1235
- patternEnd + delimiter.width,
1236
- replacement.closingOffset,
1237
- delimiter,
1238
- syntaxProfile,
1239
- );
1240
- const flagsOffset = replacement.closingOffset + delimiter.width;
1241
- const flagsLineEnd =
1242
- flagsOffset <= lineEnd ? lineEnd : findLineEnd(text, flagsOffset);
1243
- const result = scanSubstituteFlags(
1244
- text,
1245
- flagsOffset,
1246
- flagsLineEnd,
1247
- recoverAtClosingBrace,
1248
- syntaxProfile,
1249
- );
1250
- if (invalidReplacementControlEscape !== null) {
1251
- return {
1252
- ...result,
1253
- problem: issue(
1254
- "replacement-invalid-control-escape",
1255
- messages.invalidControlEscape,
1256
- invalidReplacementControlEscape.startOffset,
1257
- invalidReplacementControlEscape.endOffset,
1258
- ),
1259
- regexpProblems: patternProblems,
1260
- };
1261
- }
1262
-
1263
- if (
1264
- result.problem === null &&
1265
- syntaxProfile.dialect === "gnu" &&
1266
- patternEnd === delimiterOffset + delimiter.width &&
1267
- result.regexpModifiers.length > 0
1268
- ) {
1269
- return {
1270
- ...result,
1271
- problem: issue(
1272
- "substitute-empty-regexp-modifiers",
1273
- messages.emptySubstituteRegexpModifiers,
1274
- result.regexpModifiers[0].startOffset,
1275
- result.regexpModifiers.at(-1).endOffset,
1276
- ),
1277
- regexpProblems: patternProblems,
1278
- };
1279
- }
1280
-
1281
- return {
1282
- ...result,
1283
- regexpProblems: patternProblems,
1284
- };
1285
- }
1286
-
1287
- function addressLimitProblem(command, definition, addresses) {
1288
- if (addresses.length <= definition.maximumAddresses) {
1289
- return null;
1290
- }
1291
-
1292
- const firstExcessAddress = addresses[definition.maximumAddresses];
1293
- return issue(
1294
- "address-too-many",
1295
- messages.tooManyAddresses(command, definition.maximumAddresses),
1296
- firstExcessAddress.startOffset,
1297
- firstExcessAddress.endOffset,
1298
- );
1299
- }
1300
-
1301
- function universalAddressLimitProblem(addresses) {
1302
- if (addresses.length <= 2) {
1303
- return null;
1304
- }
1305
-
1306
- const firstExcessAddress = addresses[2];
1307
- return issue(
1308
- "address-too-many",
1309
- messages.tooManyAddressesBeforeCommand,
1310
- firstExcessAddress.startOffset,
1311
- firstExcessAddress.endOffset,
1312
- );
1313
- }
1314
-
1315
- function emptyCommandAddressLimitProblem(addresses) {
1316
- if (addresses.length === 0) {
1317
- return null;
1318
- }
1319
-
1320
- return issue(
1321
- "address-too-many",
1322
- messages.emptyCommandHasAddresses,
1323
- addresses[0].startOffset,
1324
- addresses[0].endOffset,
1325
- );
1326
- }
1327
-
1328
- function missingCommandProblem(command) {
1329
- if (command.hasOmittedAddress) {
1330
- return null;
1331
- }
1332
-
1333
- if (command.negationOffset !== null) {
1334
- return issue(
1335
- "command-missing",
1336
- messages.missingCommand,
1337
- command.negationOffset,
1338
- command.negationOffset + 1,
1339
- );
1340
- }
1341
-
1342
- const lastAddress = command.addresses.at(-1);
1343
- if (lastAddress === undefined) {
1344
- return null;
1345
- }
1346
-
1347
- return issue(
1348
- "command-missing",
1349
- messages.missingCommand,
1350
- lastAddress.startOffset,
1351
- lastAddress.endOffset,
1352
- );
1353
- }
1354
-
1355
- export function analyze(text, options = defaultSyntaxProfile) {
1356
- const syntaxProfile = requireSyntaxProfile(options);
1357
- const syntaxPolicy = syntaxPolicyFor(syntaxProfile);
1358
- const commandDefinitions = commandDefinitionsFor(syntaxProfile);
1359
- const problems = [];
1360
- const openingBraces = [];
1361
- let lineStart = 0;
1362
- let consumedUntil = -1;
1363
- let continuedOpaqueArgument = null;
1364
- let pendingCommandOffset = null;
1365
- let pendingCommandBoundary = null;
1366
-
1367
- while (lineStart <= text.length) {
1368
- const newlineOffset = text.indexOf("\n", lineStart);
1369
- const nextLineStart =
1370
- newlineOffset === -1 ? text.length + 1 : newlineOffset + 1;
1371
- let lineEnd = newlineOffset === -1 ? text.length : newlineOffset;
1372
- if (lineEnd > lineStart && text[lineEnd - 1] === "\r") {
1373
- lineEnd -= 1;
1374
- }
1375
-
1376
- const resumesOnThisLine =
1377
- pendingCommandOffset !== null &&
1378
- pendingCommandOffset >= lineStart &&
1379
- pendingCommandOffset <= lineEnd;
1380
- const initialCommandOffset = resumesOnThisLine
1381
- ? pendingCommandOffset
1382
- : lineStart;
1383
- const initialCommandBoundary = resumesOnThisLine
1384
- ? pendingCommandBoundary
1385
- : "separated";
1386
- if (resumesOnThisLine) {
1387
- pendingCommandOffset = null;
1388
- pendingCommandBoundary = null;
1389
- }
1390
-
1391
- if (!resumesOnThisLine && lineStart <= consumedUntil) {
1392
- // A multiline construct may already have scanned a later physical line.
1393
- } else if (!resumesOnThisLine && continuedOpaqueArgument !== null) {
1394
- const hasPhysicalTextLine =
1395
- newlineOffset !== -1 || lineStart < text.length;
1396
- if (!hasPhysicalTextLine) {
1397
- if (!continuedOpaqueArgument.allowsMissingLine) {
1398
- problems.push(
1399
- issue(
1400
- "text-missing-line",
1401
- messages.missingTextLine(continuedOpaqueArgument.command),
1402
- continuedOpaqueArgument.expectedLineOffset,
1403
- continuedOpaqueArgument.expectedLineOffset + 1,
1404
- ),
1405
- );
1406
- }
1407
- continuedOpaqueArgument = null;
1408
- } else if (
1409
- newlineOffset !== -1 &&
1410
- hasUnescapedTrailingBackslash(text, lineStart, lineEnd)
1411
- ) {
1412
- continuedOpaqueArgument = {
1413
- allowsMissingLine: continuedOpaqueArgument.allowsMissingLine,
1414
- command: continuedOpaqueArgument.command,
1415
- expectedLineOffset: lineEnd - 1,
1416
- };
1417
- } else {
1418
- continuedOpaqueArgument = null;
1419
- }
1420
- } else {
1421
- const initialCommand = scanEmptyCommands(
1422
- text,
1423
- initialCommandOffset,
1424
- lineEnd,
1425
- initialCommandBoundary,
1426
- );
1427
- let commandSearchOffset = initialCommand.offset;
1428
- let commandBoundary = initialCommand.boundary;
1429
-
1430
- while (commandSearchOffset < lineEnd) {
1431
- const command = findCommand(
1432
- text,
1433
- commandSearchOffset,
1434
- lineEnd,
1435
- syntaxProfile,
1436
- );
1437
- if (command.lineEnd > lineEnd) {
1438
- lineEnd = command.lineEnd;
1439
- consumedUntil = Math.max(consumedUntil, lineEnd);
1440
- }
1441
- problems.push(...command.addressProblems);
1442
- problems.push(...command.regexpProblems);
1443
-
1444
- if (command.resumeOffset !== null) {
1445
- if (command.problem !== null) {
1446
- problems.push(command.problem);
1447
- }
1448
- commandSearchOffset = command.resumeOffset;
1449
- commandBoundary = "separated";
1450
- continue;
1451
- }
1452
-
1453
- const commandOffset = command.commandOffset;
1454
- if (commandOffset === null) {
1455
- if (command.problem !== null) {
1456
- problems.push(command.problem);
1457
- } else if (command.addressProblems.length === 0) {
1458
- const addressProblem = command.hasOmittedAddress
1459
- ? null
1460
- : universalAddressLimitProblem(command.addresses);
1461
- if (addressProblem !== null) {
1462
- problems.push(addressProblem);
1463
- } else {
1464
- const missingProblem = missingCommandProblem(command);
1465
- if (missingProblem !== null) {
1466
- problems.push(missingProblem);
1467
- }
1468
- }
1469
- }
1470
- break;
1471
- }
1472
-
1473
- const commandCharacter = characterAt(text, commandOffset);
1474
- if (commandCharacter === null) {
1475
- break;
1476
- }
1477
-
1478
- if (commandCharacter.value === ";") {
1479
- if (command.problem !== null) {
1480
- problems.push(command.problem);
1481
- } else if (command.negationOffset !== null) {
1482
- const missingProblem = missingCommandProblem(command);
1483
- if (missingProblem !== null) {
1484
- problems.push(missingProblem);
1485
- }
1486
- } else if (command.addressProblems.length === 0) {
1487
- const addressProblem = command.hasOmittedAddress
1488
- ? null
1489
- : emptyCommandAddressLimitProblem(command.addresses);
1490
- if (addressProblem !== null) {
1491
- problems.push(addressProblem);
1492
- }
1493
- }
1494
- const nextCommand = scanEmptyCommands(
1495
- text,
1496
- commandOffset,
1497
- lineEnd,
1498
- commandBoundary,
1499
- );
1500
- commandSearchOffset = nextCommand.offset;
1501
- commandBoundary = nextCommand.boundary;
1502
- continue;
1503
- }
1504
-
1505
- const closesAfterMissingCommand =
1506
- commandCharacter.value === "}" && command.negationOffset !== null;
1507
- if (closesAfterMissingCommand) {
1508
- if (command.problem !== null) {
1509
- problems.push(command.problem);
1510
- } else {
1511
- const missingProblem = missingCommandProblem(command);
1512
- if (missingProblem !== null) {
1513
- problems.push(missingProblem);
1514
- }
1515
- }
1516
- }
1517
-
1518
- const definition = commandDefinitions.get(commandCharacter.value);
1519
- if (definition === undefined) {
1520
- if (command.problem !== null) {
1521
- problems.push(command.problem);
1522
- } else {
1523
- const addressProblem = command.hasOmittedAddress
1524
- ? null
1525
- : universalAddressLimitProblem(command.addresses);
1526
- if (addressProblem !== null) {
1527
- problems.push(addressProblem);
1528
- }
1529
- problems.push(
1530
- issue(
1531
- "command-unknown",
1532
- messages.unknownCommand(commandCharacter.value),
1533
- commandOffset,
1534
- commandOffset + commandCharacter.width,
1535
- ),
1536
- );
1537
- }
1538
-
1539
- const recovery = scanCommandRecoverySyntax(
1540
- text,
1541
- commandOffset + commandCharacter.width,
1542
- lineEnd,
1543
- syntaxProfile,
1544
- { recoverAtClosingBrace: openingBraces.length > 0 },
1545
- );
1546
- if (recovery.nextCommandOffset === null) {
1547
- break;
1548
- }
1549
-
1550
- const nextCommand = scanEmptyCommands(
1551
- text,
1552
- recovery.nextCommandOffset,
1553
- lineEnd,
1554
- recovery.nextCommandBoundary,
1555
- );
1556
- commandSearchOffset = nextCommand.offset;
1557
- commandBoundary = nextCommand.boundary;
1558
- continue;
1559
- }
1560
-
1561
- if (!closesAfterMissingCommand) {
1562
- const addressProblem = command.hasOmittedAddress
1563
- ? null
1564
- : command.addressProblems.length === 0
1565
- ? addressLimitProblem(
1566
- commandCharacter.value,
1567
- definition,
1568
- command.addresses,
1569
- )
1570
- : null;
1571
- if (
1572
- command.problem !== null &&
1573
- (command.problem.code !== "address-too-many" ||
1574
- addressProblem === null)
1575
- ) {
1576
- problems.push(command.problem);
1577
- } else if (addressProblem !== null) {
1578
- problems.push(addressProblem);
1579
- }
1580
- }
1581
-
1582
- if (definition.kind === "text") {
1583
- const hasPhysicalNewline =
1584
- text[lineEnd] === "\n" ||
1585
- (text[lineEnd] === "\r" && text[lineEnd + 1] === "\n");
1586
- const result = diagnoseTextCommand(
1587
- text,
1588
- commandOffset,
1589
- lineEnd,
1590
- hasPhysicalNewline,
1591
- syntaxProfile,
1592
- );
1593
- if (result.problem !== null) {
1594
- problems.push(result.problem);
1595
- }
1596
- continuedOpaqueArgument = result.textArgument;
1597
- break;
1598
- } else if (definition.kind === "shell") {
1599
- const hasPhysicalNewline =
1600
- text[lineEnd] === "\n" ||
1601
- (text[lineEnd] === "\r" && text[lineEnd + 1] === "\n");
1602
- continuedOpaqueArgument = scanShellCommand(
1603
- text,
1604
- commandOffset,
1605
- lineEnd,
1606
- hasPhysicalNewline,
1607
- syntaxProfile,
1608
- );
1609
- break;
1610
- } else if (definition.kind === "file") {
1611
- const problem = diagnoseFileCommand(
1612
- text,
1613
- commandOffset,
1614
- lineEnd,
1615
- syntaxProfile,
1616
- );
1617
- if (problem !== null) {
1618
- problems.push(problem);
1619
- }
1620
- break;
1621
- } else if (definition.kind === "label") {
1622
- const result = diagnoseLabelCommand(
1623
- text,
1624
- commandOffset,
1625
- lineEnd,
1626
- openingBraces.length > 0,
1627
- syntaxProfile,
1628
- );
1629
- if (result.problem !== null) {
1630
- problems.push(result.problem);
1631
- }
1632
- if (result.nextCommandOffset === null) {
1633
- break;
1634
- }
1635
- const nextCommand = scanEmptyCommands(
1636
- text,
1637
- result.nextCommandOffset,
1638
- lineEnd,
1639
- result.nextCommandBoundary,
1640
- );
1641
- commandSearchOffset = nextCommand.offset;
1642
- commandBoundary = nextCommand.boundary;
1643
- } else if (definition.kind === "version") {
1644
- const result = diagnoseVersionCommand(
1645
- text,
1646
- commandOffset,
1647
- lineEnd,
1648
- syntaxProfile,
1649
- );
1650
- if (result.problem !== null) {
1651
- problems.push(result.problem);
1652
- }
1653
- if (result.nextCommandOffset === null) {
1654
- break;
1655
- }
1656
- const nextCommand = scanEmptyCommands(
1657
- text,
1658
- result.nextCommandOffset,
1659
- lineEnd,
1660
- result.nextCommandBoundary,
1661
- );
1662
- commandSearchOffset = nextCommand.offset;
1663
- commandBoundary = nextCommand.boundary;
1664
- } else if (definition.kind === "substitute") {
1665
- const result = diagnoseSubstitute(
1666
- text,
1667
- commandOffset,
1668
- lineEnd,
1669
- openingBraces.length > 0,
1670
- syntaxProfile,
1671
- );
1672
- consumedUntil = result.consumedUntil;
1673
- problems.push(...(result.regexpProblems ?? []));
1674
- if (result.problem !== null) {
1675
- problems.push(result.problem);
1676
- }
1677
-
1678
- if (result.nextCommandOffset === null) {
1679
- break;
1680
- }
1681
-
1682
- if (result.nextCommandOffset > lineEnd) {
1683
- pendingCommandOffset = result.nextCommandOffset;
1684
- pendingCommandBoundary = result.nextCommandBoundary;
1685
- break;
1686
- }
1687
-
1688
- const nextCommand = scanEmptyCommands(
1689
- text,
1690
- result.nextCommandOffset,
1691
- lineEnd,
1692
- result.nextCommandBoundary,
1693
- );
1694
- commandSearchOffset = nextCommand.offset;
1695
- commandBoundary = nextCommand.boundary;
1696
- } else if (definition.kind === "block-open") {
1697
- openingBraces.push(commandOffset);
1698
- const nextCommand = scanEmptyCommands(
1699
- text,
1700
- commandOffset + 1,
1701
- lineEnd,
1702
- "direct",
1703
- );
1704
- commandSearchOffset = nextCommand.offset;
1705
- commandBoundary = nextCommand.boundary;
1706
- } else if (definition.kind === "block-close") {
1707
- const hasMatchingOpeningBrace = openingBraces.length > 0;
1708
- const hasPrimaryCommandProblem =
1709
- command.problem !== null || command.negationOffset !== null;
1710
-
1711
- if (
1712
- hasMatchingOpeningBrace &&
1713
- commandBoundary === "direct" &&
1714
- !syntaxPolicy.commandEnd.closingBraceTerminates &&
1715
- !hasPrimaryCommandProblem
1716
- ) {
1717
- problems.push(
1718
- issue(
1719
- "block-closing-brace-missing-separator",
1720
- messages.missingClosingBraceSeparator,
1721
- commandOffset,
1722
- commandOffset + 1,
1723
- ),
1724
- );
1725
- }
1726
-
1727
- if (!hasMatchingOpeningBrace) {
1728
- problems.push(
1729
- issue(
1730
- "block-unexpected-closing-brace",
1731
- messages.unexpectedClosingBrace,
1732
- commandOffset,
1733
- commandOffset + 1,
1734
- ),
1735
- );
1736
- } else {
1737
- openingBraces.pop();
1738
- }
1739
-
1740
- const result = simpleCommandEnd(
1741
- text,
1742
- commandCharacter.value,
1743
- commandOffset,
1744
- lineEnd,
1745
- syntaxProfile,
1746
- { recoverAtClosingBrace: true },
1747
- );
1748
- if (result.problem !== null) {
1749
- problems.push(result.problem);
1750
- }
1751
- if (result.nextCommandOffset === null) {
1752
- break;
1753
- }
1754
- const nextCommand = scanEmptyCommands(
1755
- text,
1756
- result.nextCommandOffset,
1757
- lineEnd,
1758
- result.nextCommandBoundary,
1759
- );
1760
- commandSearchOffset = nextCommand.offset;
1761
- commandBoundary = nextCommand.boundary;
1762
- } else if (definition.kind === "transliterate") {
1763
- const transliterateLineEnd =
1764
- commandOffset + 1 < lineEnd &&
1765
- text[commandOffset + 1] === "\r" &&
1766
- text[lineEnd] === "\r"
1767
- ? lineEnd + 1
1768
- : lineEnd;
1769
- const result = diagnoseTransliterate(
1770
- text,
1771
- commandOffset,
1772
- transliterateLineEnd,
1773
- openingBraces.length > 0,
1774
- syntaxProfile,
1775
- );
1776
- if (result.problem !== null) {
1777
- problems.push(result.problem);
1778
- }
1779
- if (result.nextCommandOffset === null) {
1780
- break;
1781
- }
1782
- const nextCommand = scanEmptyCommands(
1783
- text,
1784
- result.nextCommandOffset,
1785
- lineEnd,
1786
- result.nextCommandBoundary,
1787
- );
1788
- commandSearchOffset = nextCommand.offset;
1789
- commandBoundary = nextCommand.boundary;
1790
- } else if (
1791
- definition.kind === "simple" ||
1792
- definition.kind === "numeric"
1793
- ) {
1794
- const commandEnd =
1795
- definition.kind === "numeric"
1796
- ? scanOptionalNumericArgumentSyntax(
1797
- text,
1798
- commandOffset,
1799
- lineEnd,
1800
- syntaxProfile,
1801
- ).commandEndOffset
1802
- : commandOffset + commandCharacter.width;
1803
- const result = diagnoseCommandEnd(
1804
- text,
1805
- commandCharacter.value,
1806
- commandEnd,
1807
- lineEnd,
1808
- syntaxProfile,
1809
- { insideBlock: openingBraces.length > 0 },
1810
- );
1811
- if (result.problem !== null) {
1812
- problems.push(result.problem);
1813
- }
1814
- if (result.nextCommandOffset === null) {
1815
- break;
1816
- }
1817
- const nextCommand = scanEmptyCommands(
1818
- text,
1819
- result.nextCommandOffset,
1820
- lineEnd,
1821
- result.nextCommandBoundary,
1822
- );
1823
- commandSearchOffset = nextCommand.offset;
1824
- commandBoundary = nextCommand.boundary;
1825
- } else {
1826
- break;
1827
- }
1828
- }
1829
- }
1830
-
1831
- if (newlineOffset === -1) {
1832
- break;
1833
- }
1834
- lineStart = nextLineStart;
1835
- }
1836
-
1837
- for (const openingBrace of openingBraces) {
1838
- problems.push(
1839
- issue(
1840
- "block-unclosed-opening-brace",
1841
- messages.unclosedOpeningBrace,
1842
- openingBrace,
1843
- openingBrace + 1,
1844
- ),
1845
- );
1846
- }
1847
-
1848
- return problems.map((problem) => ({
1849
- ...problem,
1850
- message: renderMessage(problem.message, syntaxProfile),
1851
- }));
1852
- }
1853
-
1854
- export function createDiagnostics(document, options = defaultSyntaxProfile) {
1855
- return analyze(document.getText(), options).map((problem) => ({
1856
- severity: DiagnosticSeverity.Error,
1857
- range: {
1858
- start: document.positionAt(problem.startOffset),
1859
- end: document.positionAt(problem.endOffset),
1860
117
  },
1861
- message: problem.message,
1862
- code: problem.code,
1863
- source: SOURCE,
1864
- }));
118
+ );
1865
119
  }