@tsuzuri-lab/sed-language-server 0.1.1 → 0.2.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 +0,0 @@
1
- import { DiagnosticSeverity } from "vscode-languageserver/node";
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";
33
-
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
- }
102
-
103
- const count = maximum === 1 ? "one address" : "two addresses";
104
- return `The ${DIALECT_NAME} sed \`${command}\` command accepts at most ${count}.`;
105
- },
106
- };
107
-
108
- function renderMessage(message, syntaxProfile) {
109
- const dialectName = syntaxProfile.dialect === "gnu" ? "GNU" : "POSIX";
110
- return message.replaceAll(DIALECT_NAME, dialectName);
111
- }
112
-
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
- }
179
- }
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;
190
- }
191
-
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") {
211
- return {
212
- endOffset: result.endOffset ?? end,
213
- kind: "invalid",
214
- problem: null,
215
- };
216
- }
217
-
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
- 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
- ),
292
- };
293
- }
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 [];
317
- }
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;
352
- }
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;
376
- }
377
-
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
- }
965
-
966
- if (
967
- policy.substituteFlags.separator === "optional-blanks" &&
968
- (character.value === " " || character.value === "\t")
969
- ) {
970
- cursor = skipBlanks(text, cursor, lineEnd);
971
- continue;
972
- }
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
- }
1110
-
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
- };
1126
- }
1127
-
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
- }
1163
-
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
- ) {
1182
- return {
1183
- problem: null,
1184
- consumedUntil: lineEnd,
1185
- nextCommandOffset: null,
1186
- };
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
- },
1861
- message: problem.message,
1862
- code: problem.code,
1863
- source: SOURCE,
1864
- }));
1865
- }