File size: 7,852 Bytes
f0634fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// test/helpers/known-differences.ts
//
// The single source of truth for the known, deliberately documented ways
// this parser's tree deviates from tree-sitter-bash 0.25.0. Every entry is:
//   - referenced by @known-diff samples in test/fixtures/differential/ and
//     test/fixtures/corpus/known-diffs.txt (the stored dump in each sample
//     pins the exact deviation shape — if our output drifts, or starts
//     matching the reference, the differential tests fail);
//   - anchored in README.md's "Known differences" section: `readmeAnchor`
//     must appear there verbatim, so the list and the README cannot
//     silently drift apart (enforced by test/differential.test.ts).

export interface KnownDifference {
  /** Identifier referenced from fixture @known-diff directives. */
  readonly id: string;
  /** Verbatim substring of README.md's Known differences section. */
  readonly readmeAnchor: string;
  /** Short human-readable description. */
  readonly summary: string;
}

export const KNOWN_DIFFERENCES: readonly KnownDifference[] = [
  {
    id: 'redirect-readwrite',
    readmeAnchor: '`<>` (read-write redirect) is parsed as a normal `file_redirect`',
    summary: '<> parses as file_redirect; the reference fails to parse it.',
  },
  {
    id: 'heredoc-multiple-on-line',
    readmeAnchor: 'Several heredocs on one line',
    summary: 'cat <<A <<B: the second << degrades to an ERROR node.',
  },
  {
    id: 'heredoc-tail-statements',
    readmeAnchor: 'Statements after a heredoc on the same line',
    summary: 'cat <<EOF; echo x — the tail is absorbed into heredoc_redirect (the reference errors).',
  },
  {
    id: 'heredoc-content-chunks',
    readmeAnchor: 'every plain text chunk — including the leading one — becomes a `heredoc_content` node',
    summary: 'Unquoted heredoc_body: leading content chunk and backtick substitutions are nodes here.',
  },
  {
    id: 'heredoc-at-statement-start',
    readmeAnchor: 'A heredoc redirect at statement start',
    summary: '<<EOF cat parses normally; the reference errors.',
  },
  {
    id: 'heredoc-body-dollar-quote',
    readmeAnchor: "A `$'` sequence inside an unquoted heredoc body",
    summary: "$' inside a heredoc body is plain text here; the reference errors when it starts a body line.",
  },
  {
    id: 'recovery-partial-nodes',
    readmeAnchor: 'Unterminated or invalid constructs keep their partial nodes',
    summary: 'Invalid input keeps partial nodes with hasError; the reference degrades to ERROR nodes differently.',
  },
  {
    id: 'trailing-connector',
    readmeAnchor: 'A trailing connector (`ls &&`, `ls |`)',
    summary: 'Trailing && / | yields a single-child list/pipeline with hasError.',
  },
  {
    id: 'empty-backtick',
    readmeAnchor: 'An empty backtick substitution',
    summary: '`` (and whitespace-only pairs) parse as an empty command_substitution; the reference has a single `` token.',
  },
  {
    id: 'empty-command-substitution',
    readmeAnchor: 'An empty command substitution (`$( )`)',
    summary: '$( ) is a clean empty command_substitution; the reference inserts a zero-width command.',
  },
  {
    id: 'arithmetic-hex',
    readmeAnchor: 'a hex literal is a `number`',
    summary: '$((0x1F)) → number "0x1F"; the reference produces variable_name (a quirk).',
  },
  {
    id: 'test-paren-group-logical',
    readmeAnchor: '`[[ ((a) == x) && y ]]`',
    summary: 'Parenthesized test group followed by && parses cleanly; the reference mis-reads it.',
  },
  {
    id: 'test-extglob-rejected-group',
    readmeAnchor: 'An extglob group the reference rejects',
    summary: 'Groups after a literal or dot (x@(y|z)w, *.@(a)) are errors in the reference; recovery shapes differ.',
  },
  {
    id: 'test-negative-decimal',
    readmeAnchor: 'A negative decimal operand (`[[ $n == -0.5 ]]`)',
    summary: 'This parser produces extglob_pattern "-0.5"; the reference a unary_expression.',
  },
  {
    id: 'test-fused-operator-expansion',
    readmeAnchor: 'A test operator fused with an expansion (`[[ -x$f && … ]]`)',
    summary: 'Flat concatenation of -x and $f here; the reference wraps a unary_expression.',
  },
  {
    id: 'test-escaped-pipe-pattern',
    readmeAnchor: 'An escaped `|` inside a pattern (`[[ $a == foo\\|bar ]]`)',
    summary: 'One extglob_pattern "foo\\|bar" here; the reference splits the expression.',
  },
  {
    id: 'test-pattern-two-constructs',
    readmeAnchor: 'A comparison right side with TWO substitutions or quotes',
    summary: '*${x}*${y} / *"s"*"t" — the reference recovers with a nested binary_expression; shapes differ.',
  },
  {
    id: 'string-content-newlines',
    readmeAnchor: '`string_content` is not split at newlines',
    summary: 'Multiline strings keep one string_content; the reference splits it.',
  },
  {
    id: 'expansion-bang-hash-special',
    readmeAnchor: '`${!# }` and `${!## }`',
    summary: 'Pathological ${!#…} forms: the reference recovers with zero-width tokens; shapes differ.',
  },
  {
    id: 'number-base-with-expansion',
    readmeAnchor: 'A base prefix fused with an expansion (`10#${x}`)',
    summary: 'The reference wraps the whole thing in a number node (a quirk); this parser a concatenation.',
  },
  {
    id: 'expansion-default-array',
    readmeAnchor: '`${v:-(default)}`',
    summary: 'A parenthesized default value is a word here; the reference parses an array node (a quirk).',
  },
  {
    id: 'escaped-space-argument',
    readmeAnchor: 'An escaped space or tab between arguments',
    summary: 'The reference drops escaped spaces/tabs as extras (its tree has gaps, words split); this parser keeps them in the word.',
  },
  {
    id: 'expansion-equals-operator',
    readmeAnchor: '`${=1}`',
    summary: 'The zsh-style ${=…} form: the reference produces variable_name; this parser a word.',
  },
  {
    id: 'dollar-backtick-substitution',
    readmeAnchor: 'A `$` directly fused with a backtick substitution',
    summary: '$`cmd` is one command_substitution with a $` token in the reference; here $ + command_substitution.',
  },
  {
    id: 'cfor-compound-assign-negative',
    readmeAnchor: 'A negative literal after a compound assignment in a c-style for header',
    summary: 'j *= -1: the reference folds the sign into a number "-1"; this parser a unary_expression.',
  },
  {
    id: 'case-short-option-pattern',
    readmeAnchor: 'A single-dash short-option case pattern',
    summary: '-o) is a word here (its usual reference form); the reference flips to extglob_pattern in some scanner states.',
  },
  {
    id: 'case-pattern-after-continuation',
    readmeAnchor: 'A case pattern directly after a line continuation',
    summary: 'A pattern after \\<newline> is a word in the reference (scanner-state quirk); a continuation INSIDE a pattern is an error there. This parser classifies by content and keeps continuations.',
  },
  {
    id: 'nonascii-identifier-assignment',
    readmeAnchor: 'A non-ASCII “identifier” in assignment position',
    summary: '变量=值 is a hasError variable_assignment in the reference; this parser keeps it a plain command word (bash itself rejects such names).',
  },
  {
    id: 'case-rejected-group',
    readmeAnchor: 'An extglob group the reference rejects in a case pattern',
    summary: 'x@(y)): the reference reparses the group as a new case item; this parser degrades the whole pattern to ERROR.',
  },
  {
    id: 'expansion-replacement-escaped-backslash',
    readmeAnchor: 'A double backslash in a replacement value (`${x// /\\\\|}`)',
    summary: 'The reference drops the first backslash (a quirk); this parser keeps both.',
  },
];

const KNOWN_DIFFERENCE_IDS: ReadonlySet<string> = new Set(KNOWN_DIFFERENCES.map((entry) => entry.id));

export function isKnownDifferenceId(id: string): boolean {
  return KNOWN_DIFFERENCE_IDS.has(id);
}