Skip to content

Commit ebf39e7

Browse files
authored
Merge pull request #37 from theodevelop/dev
release: v1.5.1
2 parents 725275e + 2cd1c07 commit ebf39e7

10 files changed

Lines changed: 327 additions & 37 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@ dist/
33
client/out/
44
server/out/
55
*.vsix
6-
.env
6+
.env
7+
tests/_*
8+
docs/
9+
.vscode/

.vscodeignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ package-lock.json
2424
.gitattributes
2525
.env
2626
.env.*
27+
docs/

CHANGELOG.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
All notable changes to the **Bison/Flex Language Support** extension will be documented in this file.
44

5+
## [1.5.1] - 2026-04-02
6+
7+
### Fixed
8+
9+
- **Flex — escaped quotes in quoted string patterns** (#30): patterns like `X"\'"` and `Y"\""` no longer trigger false `flex/invalid-pattern` errors. The validator now correctly handles `\"` and `\'` escape sequences inside Flex quoted strings.
10+
- **Flex — abbreviation refs on rule lines with no inline action** (#31): `{ABBR}` used after a `^` BOL anchor or on a rule line whose action block appears on the following line was not recorded as an abbreviation reference, causing false `flex/unused-abbrev` warnings.
11+
- **Flex — quoted strings with spaces in `rawPattern`** (audit-A): patterns like `"hello world"` were truncated at the space inside the quoted literal, causing false `flex/unreachable-rule` duplicates for distinct patterns sharing a common word prefix. `rawPattern()` now tracks quoted-string depth.
12+
- **Flex — standalone `{` as multi-line action opener** (audit-B): a `{` appearing alone on the line after a rule pattern (valid Flex multi-line action syntax) was pushed as a spurious rule entry with pattern `{`, producing false `flex/unreachable-rule` diagnostics for every subsequent multi-line-action rule.
13+
- **Flex — lowercase start condition names** (audit-C): all start-condition regex patterns used `[A-Z_][A-Z0-9_]*` (uppercase only). SC names that are valid C identifiers but lowercase (e.g. `%x comment`) were silently ignored, skipping `flex/undefined-sc` and `flex/unused-sc` diagnostics for them entirely.
14+
- **Flex — single-tab action separator in abbreviation ref scan** (audit-D): the heuristic that separates the pattern from the action used `\s{2,}`, which did not match a single-tab separator. `{identifier}` tokens inside the C action body (e.g. compound literals) were falsely counted as abbreviation references, suppressing `flex/unused-abbrev`.
15+
- **Cleanup**: removed two dead entries in the catch-all pattern set that contained a literal newline character and could never match a rule line.
16+
- **Bison — lowercase/mixed-case tokens in precedence declarations** (audit-E): `%left`/`%right`/`%nonassoc` used an uppercase-only regex `[A-Z_][A-Z0-9_]*`, silently dropping tokens like `kPLUS` or `tTOKEN` from the precedence table. This caused false `bison/undeclared-token` warnings and incorrect shift/reduce heuristic results for such tokens.
17+
- **Bison — `$N` references after nested sub-blocks in inline actions** (audit-F): the `extractDollarRefs` scanner used `/\{([^}]*)\}/` which stops at the first `}`, missing `$N` references that appear after a nested `{ … }` block inside the same action (e.g. `{ if (cond) { log(); } $$ = $5; }`). Replaced with a brace-depth scanner; the same fix was applied to `extractSymbols`, `getFirstSymbol`, and `extractRuleReferences` for consistency.
18+
19+
---
20+
521
## [1.5.0] - 2026-04-01
622

723
### Added
@@ -11,7 +27,7 @@ All notable changes to the **Bison/Flex Language Support** extension will be doc
1127
- **`Bison/Flex: Show in Generated File`** — from a `.y` / `.l` source, locates the generated file (using `bisonFlex.buildDirectory` setting, CMake detection, Makefile detection, same-directory fallback, then workspace-wide search) and navigates to the matching line. A QuickPick is shown when multiple candidates are found.
1228
- New setting `bisonFlex.buildDirectory`: optional path to the build output directory, used by **Show in Generated File** to locate generated files when they are not in the same directory as the source.
1329

14-
--
30+
---
1531

1632
## [1.4.1] - 2026-03-31
1733

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,44 @@ Real-time error detection as you type:
3939
| Shift/reduce conflict heuristic | |
4040
| Unknown/invalid directive | |
4141

42+
Every diagnostic carries a **source** field (`bison` / `flex`), a **code slug** (e.g. `bison/unused-token`), and where available a **link** to the GNU documentation — rendered as a clickable `[bison/unused-token]` link in the Problems panel. Unused symbols are rendered greyed-out via `DiagnosticTag.Unnecessary`.
43+
44+
### Fix-it Hints (Quick Fixes)
45+
46+
22 code actions available via the lightbulb (`Ctrl+.`) or directly from the Problems panel:
47+
48+
**Bison** (11 fixes):
49+
- Insert missing `%%` separator
50+
- Declare undeclared `%token`
51+
- Insert `%empty` for empty production
52+
- Remove unused token declaration
53+
- Remove unknown directive
54+
- Add rule stub for missing non-terminal
55+
- Add `%type <todo>` declaration
56+
- Remove invalid `%start` / Add `%start` directive
57+
- Close unclosed `%{` block
58+
- Migrate Yacc legacy directives (`%error-verbose``%define parse.error verbose`, `%name-prefix`, `%pure-parser`, `%binary`)
59+
60+
**Flex** (11 fixes):
61+
- Insert missing `%%` separator
62+
- Define abbreviation stub
63+
- Remove unused abbreviation
64+
- Remove unused start condition
65+
- Remove unknown directive
66+
- Declare `%x SC_NAME` for undefined start condition
67+
- Remove unused `%option`
68+
- Remove duplicate `<<EOF>>` rule
69+
- Add `%option noyywrap`
70+
- Close unclosed `%{` block
71+
- Remove inaccessible rule
72+
73+
### Source ↔ Generated File Navigation
74+
75+
Jump between Bison/Flex grammar sources and their generated C files using `#line` directives:
76+
77+
- **Bison/Flex: Show in Source** — from a generated `.tab.c` / `lex.yy.c` file, reads the nearest `#line N "file.y"` directive above the cursor and opens the grammar source at the correct line. Appears in the context menu only when a generated file is detected.
78+
- **Bison/Flex: Show in Generated File** — from a `.y` / `.l` source, locates the generated file and navigates to the matching line. Searches `bisonFlex.buildDirectory`, then CMake/Makefile detection, then the same directory, then a workspace-wide scan. A QuickPick is shown when multiple candidates are found.
79+
4280
### Autocompletion
4381

4482
Context-aware suggestions triggered as you type:
@@ -185,6 +223,10 @@ Then press `F5` in VS Code to launch the Extension Development Host.
185223
| `bisonFlex.showInlayHints` | `boolean` | `true` | Show inline type hints for `$$`/`$1`/`@$` semantic values |
186224
| `bisonFlex.enableCodeLens` | `boolean` | `true` | Show reference counts and entry-point badges above rules |
187225
| `bisonFlex.enableCmakeDiagnostics` | `boolean` | `true` | Warn when a `.y`/`.l` file is not referenced in `CMakeLists.txt` |
226+
| `bisonFlex.minVersionBison` | `string` | `""` | Suppress checks that require a newer Bison version (e.g. `"3.0"`). Fires `bison/feature-requires-version` when a `%define` feature exceeds this version. |
227+
| `bisonFlex.minVersionFlex` | `string` | `""` | Same as above for Flex. |
228+
| `bisonFlex.disabledChecks` | `array` | `[]` | Diagnostic code slugs to suppress entirely (e.g. `["bison/shift-reduce", "flex/missing-yywrap"]`). |
229+
| `bisonFlex.buildDirectory` | `string` | `""` | Path to the build output directory. Used by **Show in Generated File** to locate `.tab.c` / `lex.yy.c` when they are not next to the source. |
188230

189231
---
190232

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "bison-flex-lang",
33
"displayName": "Bison/Flex Language Support",
44
"description": "Full-featured language support for GNU Bison (.y, .yy) and Flex/RE-flex (.l, .ll) — syntax highlighting with embedded C/C++, real-time diagnostics, intelligent autocompletion, and hover documentation for all directives.",
5-
"version": "1.5.0",
5+
"version": "1.5.1",
66
"publisher": "theodevelop",
77
"license": "MIT",
88
"repository": {

server/src/parser/bisonParser.ts

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export function parseBisonDocument(text: string): BisonDocument {
166166
const precMatch = trimmed.match(/^%(left|right|nonassoc|precedence)\s+(.*)/);
167167
if (precMatch) {
168168
const kind = precMatch[1] as PrecedenceDeclaration['kind'];
169-
const rawSymbols = precMatch[2].match(/[A-Z_][A-Z0-9_]*|"[^"]*"/g) || [];
169+
const rawSymbols = precMatch[2].match(/[A-Za-z_][A-Za-z0-9_]*|"[^"]*"/g) || [];
170170
const symbols: string[] = [];
171171
const symbolRanges: Range[] = [];
172172
for (const raw of rawSymbols) {
@@ -389,6 +389,33 @@ function replaceStringLiterals(text: string): string {
389389
.replace(/'((?:[^'\\]|\\.)*)'/g, (_, content) => ` ${strLiteralPlaceholder(`'${content}'`)} `);
390390
}
391391

392+
/**
393+
* Remove all brace-balanced { ... } blocks from `text`, replacing each with `replacement`.
394+
* Handles arbitrarily nested braces, unlike /\{[^}]*\}/ which stops at the first `}`.
395+
* Unmatched `{` without a closing `}` (e.g. a multi-line action opener on its own line)
396+
* are left out of the result — the Phase 3 brace tracker handles them separately.
397+
*/
398+
function removeBalancedBraces(text: string, replacement: string = ' '): string {
399+
let result = '';
400+
let depth = 0;
401+
let pendingOpen = false; // true while inside a block that hasn't been closed yet
402+
for (let i = 0; i < text.length; i++) {
403+
if (text[i] === '{') {
404+
if (depth === 0) pendingOpen = true;
405+
depth++;
406+
} else if (text[i] === '}') {
407+
depth = Math.max(0, depth - 1);
408+
if (depth === 0 && pendingOpen) {
409+
result += replacement; // only emit placeholder when the block is fully closed
410+
pendingOpen = false;
411+
}
412+
} else if (depth === 0) {
413+
result += text[i];
414+
}
415+
}
416+
return result;
417+
}
418+
392419
/**
393420
* Extract all grammar symbols (identifiers) from a production RHS in order.
394421
*
@@ -399,8 +426,7 @@ function replaceStringLiterals(text: string): string {
399426
* `"("` apart from `"{"` (both have different placeholders).
400427
*/
401428
function extractSymbols(text: string): string[] {
402-
const cleaned = replaceStringLiterals(text)
403-
.replace(/\{[^}]*\}/g, ' __midaction__ ') // inline actions count as a symbol ($N position)
429+
const cleaned = removeBalancedBraces(replaceStringLiterals(text), ' __midaction__ ') // inline actions count as a symbol ($N position)
404430
.replace(/%prec\s+\S+/g, ' ') // remove %prec TOKEN
405431
.replace(/%empty/g, ' ') // remove %empty
406432
.replace(/\/\/.*$/g, ' ') // remove line comments
@@ -423,8 +449,7 @@ function extractSymbols(text: string): string[] {
423449
* `__s` (not all-caps) and is therefore not confused with a real terminal.
424450
*/
425451
function getFirstSymbol(text: string): string | undefined {
426-
const cleaned = replaceStringLiterals(text)
427-
.replace(/\{[^}]*\}/g, ' ') // remove inline actions
452+
const cleaned = removeBalancedBraces(replaceStringLiterals(text)) // remove inline actions
428453
.replace(/%prec\s+\S+/g, ' ') // remove %prec TOKEN
429454
.replace(/%empty/g, ' ') // remove %empty
430455
.replace(/\/\/.*$/g, ' ') // remove line comments
@@ -518,15 +543,27 @@ function parseTokenNames(text: string, type: string | undefined, lineNum: number
518543

519544
/**
520545
* Scan the inline action block(s) on a single line for $n references.
521-
* Only handles single-line { ... } blocks; multi-line actions are not detected here.
546+
* Uses a brace-depth scanner so that $n references appearing after a nested
547+
* sub-block (e.g. `{ if (x) { foo(); } $$ = $1; }`) are not missed.
548+
* Only handles single-line { ... } blocks; multi-line actions are tracked by
549+
* the caller (Phase 3 loop in parseBisonDocument).
522550
* $$ and $<type>n are deliberately skipped.
523551
*/
524552
function extractDollarRefs(text: string, lineNum: number, fullLine: string): DollarRef[] {
525553
const refs: DollarRef[] = [];
526-
const actionRegex = /\{([^}]*)\}/g;
527-
let actionMatch: RegExpExecArray | null;
528-
while ((actionMatch = actionRegex.exec(text)) !== null) {
529-
const actionContent = actionMatch[1];
554+
let i = 0;
555+
while (i < text.length) {
556+
if (text[i] !== '{') { i++; continue; }
557+
// Found the opening brace of an action block — scan to the matching '}'
558+
let depth = 1;
559+
let j = i + 1;
560+
while (j < text.length && depth > 0) {
561+
if (text[j] === '{') depth++;
562+
else if (text[j] === '}') depth--;
563+
j++;
564+
}
565+
// text[i+1 .. j-2] is the full content of this balanced action block
566+
const actionContent = text.substring(i + 1, j - 1);
530567
const dollarRegex = /\$(\d+)/g;
531568
let m: RegExpExecArray | null;
532569
while ((m = dollarRegex.exec(actionContent)) !== null) {
@@ -540,6 +577,7 @@ function extractDollarRefs(text: string, lineNum: number, fullLine: string): Dol
540577
range: Range.create(lineNum, col >= 0 ? col : 0, lineNum, (col >= 0 ? col : 0) + fullMatch.length),
541578
});
542579
}
580+
i = j; // advance past the entire balanced block
543581
}
544582
return refs;
545583
}
@@ -579,9 +617,7 @@ function extractRuleReferences(text: string, lineNum: number, fullLine: string,
579617

580618
// Find identifiers in rule bodies (potential token/nonterminal references)
581619
// Skip: strings, actions (braces), %prec keyword (but keep its token), %empty, comments
582-
const cleaned = text
583-
.replace(/"(?:[^"\\]|\\.)*"/g, '') // remove strings
584-
.replace(/\{[^}]*\}/g, '') // remove inline actions
620+
const cleaned = removeBalancedBraces(text.replace(/"(?:[^"\\]|\\.)*"/g, '')) // remove strings, then inline actions
585621
.replace(/%prec/g, '') // remove %prec keyword (keep the token name)
586622
.replace(/%empty/g, '') // remove %empty
587623
.replace(/\/\/.*$/g, ''); // remove line comments

server/src/parser/flexParser.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ export function parseFlexDocument(text: string): FlexDocument {
234234
if (closeIdx >= 0) {
235235
// Collect any additional SC names before the >
236236
const before = trimmed.substring(0, closeIdx);
237-
const moreConds = before.match(/[A-Z_][A-Z0-9_]*/g);
237+
const moreConds = before.match(/[A-Za-z_][A-Za-z0-9_]*/g);
238238
if (moreConds) pendingScHeader += ',' + moreConds.join(',');
239239
const conds = pendingScHeader.replace(/^,+/, '').split(',').filter(s => s.length > 0);
240240
pendingScHeader = null;
@@ -246,7 +246,7 @@ export function parseFlexDocument(text: string): FlexDocument {
246246
}
247247
} else {
248248
// Still accumulating conditions from this line
249-
const moreConds = trimmed.match(/[A-Z_][A-Z0-9_]*/g);
249+
const moreConds = trimmed.match(/[A-Za-z_][A-Za-z0-9_]*/g);
250250
if (moreConds) pendingScHeader += ',' + moreConds.join(',');
251251
}
252252
continue;
@@ -267,10 +267,19 @@ export function parseFlexDocument(text: string): FlexDocument {
267267
continue;
268268
}
269269

270+
// ── Multi-line action opener: bare `{` on its own line ────────────────────
271+
// In Flex, the action brace may appear on the line after the pattern.
272+
// Treat a standalone `{` as the opening of a C action block, not a rule.
273+
if (trimmed === '{') {
274+
actionDepth = 1;
275+
continue;
276+
}
277+
270278
// ── SC block opener: <SC1,SC2>{ ───────────────────────────────────────────
271279
// Single-line header: <SC1,SC2>{ or <SC1,SC2> {
280+
// SC names may be upper or lower case (any valid C identifier).
272281
{
273-
const scBlockMatch = trimmed.match(/^<([A-Z_][A-Z0-9_]*(?:,[A-Z_][A-Z0-9_]*)*)>\s*\{/);
282+
const scBlockMatch = trimmed.match(/^<([A-Za-z_][A-Za-z0-9_]*(?:,[A-Za-z_][A-Za-z0-9_]*)*)>\s*\{/);
274283
if (scBlockMatch) {
275284
const conds = scBlockMatch[1].split(',');
276285
scBlockStack.push(conds);
@@ -284,7 +293,7 @@ export function parseFlexDocument(text: string): FlexDocument {
284293
continue;
285294
}
286295
// Multi-line header start: <SC1, (no closing > on this line)
287-
const scMultiStart = trimmed.match(/^<([A-Z_][A-Z0-9_]*(?:,[A-Z_][A-Z0-9_]*)*,\s*)$/);
296+
const scMultiStart = trimmed.match(/^<([A-Za-z_][A-Za-z0-9_]*(?:,[A-Za-z_][A-Za-z0-9_]*)*,\s*)$/);
288297
if (scMultiStart) {
289298
pendingScHeader = scMultiStart[1].replace(/,\s*$/, '');
290299
continue;
@@ -293,7 +302,8 @@ export function parseFlexDocument(text: string): FlexDocument {
293302

294303
// ── Extract start condition references: <SC_NAME> or <SC1,SC2> ────────────
295304
// Exclude <<EOF>> which is a special pattern, not a start condition
296-
const scRefs = line.matchAll(/(?<!<)<([A-Z_][A-Z0-9_]*(?:,[A-Z_][A-Z0-9_]*)*)>(?!>)/g);
305+
// SC names may be upper or lower case (any valid C identifier).
306+
const scRefs = line.matchAll(/(?<!<)<([A-Za-z_][A-Za-z0-9_]*(?:,[A-Za-z_][A-Za-z0-9_]*)*)>(?!>)/g);
297307
for (const m of scRefs) {
298308
const conditions = m[1].split(',');
299309
for (const cond of conditions) {
@@ -311,8 +321,11 @@ export function parseFlexDocument(text: string): FlexDocument {
311321
const abbrRefs = line.matchAll(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g);
312322
for (const m of abbrRefs) {
313323
const name = m[1];
314-
// Only count as abbreviation ref if it appears before any action block on this line
315-
const actionStart = line.indexOf('{', (line.match(/\s{2,}\{/) || { index: line.length }).index || line.length);
324+
// Only count as abbreviation ref if it appears before any action block on this line.
325+
// If there is no action { on this line (multi-line action), treat actionStart as line.length
326+
// so all {name} refs on this line are counted.
327+
const actionMatch = line.match(/\s+\{/);
328+
const actionStart = actionMatch !== null ? line.indexOf('{', actionMatch.index!) : line.length;
316329
if (m.index !== undefined && m.index < actionStart) {
317330
const col = m.index;
318331
const range = Range.create(i, col, i, col + m[0].length);
@@ -327,7 +340,7 @@ export function parseFlexDocument(text: string): FlexDocument {
327340
// Start conditions: explicit <SC> prefix on this line PLUS any inherited from <SC>{ block
328341
const inherited = scBlockStack.length > 0 ? scBlockStack[scBlockStack.length - 1] : [];
329342
const startConditions: string[] = [...inherited];
330-
const scMatch = trimmed.match(/^<([A-Z_][A-Z0-9_]*(?:,[A-Z_][A-Z0-9_]*)*)>/);
343+
const scMatch = trimmed.match(/^<([A-Za-z_][A-Za-z0-9_]*(?:,[A-Za-z_][A-Za-z0-9_]*)*)>/);
331344
if (scMatch) {
332345
for (const c of scMatch[1].split(',')) {
333346
if (!startConditions.includes(c)) startConditions.push(c);
@@ -375,7 +388,7 @@ function parseOptions(text: string, lineNum: number, fullLine: string, doc: Flex
375388
}
376389

377390
function parseStartConditions(text: string, exclusive: boolean, lineNum: number, fullLine: string, doc: FlexDocument): void {
378-
const names = text.match(/[A-Z_][A-Z0-9_]*/g);
391+
const names = text.match(/[A-Za-z_][A-Za-z0-9_]*/g);
379392
if (!names) return;
380393
for (const name of names) {
381394
const col = fullLine.indexOf(name);

0 commit comments

Comments
 (0)