Skip to content

Commit 8787f56

Browse files
authored
Merge pull request #43 from theodevelop/dev
chore: release v1.5.2
2 parents ebf39e7 + 614f856 commit 8787f56

7 files changed

Lines changed: 235 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

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

5+
## [1.5.2] - 2026-04-05
6+
7+
### Added
8+
9+
- **Flex — Code Lens for abbreviations** (#39): abbreviation definitions in the definitions section now show a clickable "N references" Code Lens, consistent with start conditions and Bison rules. Clicking opens the References panel with all `{ABBR}` usages in the rules section.
10+
11+
### Fixed
12+
13+
- **Flex — SC refs in multi-line block headers** (#38): in `<SC_A,\nSC_B,\nSC_C>{` multi-line syntax, only the SC on the closing line was recorded as a start-condition reference; all preceding SCs had 0 refs, causing false `flex/unused-sc` diagnostics and incorrect Code Lens counts.
14+
- **Flex — abbreviation refs on indented rule lines** (#38): `{ABBR}` on a rule line indented inside a `<SC>{ }` block was not recorded as an abbreviation reference. The `actionStart` heuristic mistook the leading whitespace before `{ABBR}` itself as the action opener, making `m.index < actionStart` false for all such lines.
15+
- **Flex — transitive abbreviation references** (#38): abbreviations used only inside another abbreviation's definition (e.g. `ALNUM_LITERAL {ALNUM_LITERAL_Q}|{ALNUM_LITERAL_A}`) were never recorded as referenced, producing false `flex/unused-abbrev` for the inner abbreviations. The parser now scans each definition body for `{name}` references.
16+
- **Flex — `flex/unreachable-rule` false positive for patterns with mandatory non-class groups** (#38): `isWordPattern` returned `true` for any pattern starting with `[a-zA-Z_` regardless of what followed, so `[_0-9A-Z]+(\.[_0-9A-Z]+)+` (which requires a literal dot and cannot match a plain keyword) was treated as a general identifier pattern, producing false `flex/unreachable-rule` warnings for keyword rules that followed it. `isWordPattern` now requires the entire pattern to consist solely of character-class groups.
17+
18+
---
19+
520
## [1.5.1] - 2026-04-02
621

722
### Fixed

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.1",
5+
"version": "1.5.2",
66
"publisher": "theodevelop",
77
"license": "MIT",
88
"repository": {

server/src/parser/flexParser.ts

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,17 @@ export function parseFlexDocument(text: string): FlexDocument {
182182
pattern,
183183
location: Range.create(i, 0, i, name.length),
184184
});
185+
// Record inter-abbreviation references: if this definition body
186+
// contains {OTHER}, mark OTHER as referenced so it is not flagged
187+
// as unused just because it only appears inside another abbreviation.
188+
const patStart = line.indexOf(abbrMatch[2]);
189+
for (const ref of pattern.matchAll(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g)) {
190+
const refName = ref[1];
191+
const refCol = patStart >= 0 ? patStart + ref.index! : ref.index!;
192+
const range = Range.create(i, refCol, i, refCol + ref[0].length);
193+
if (!doc.abbreviationRefs.has(refName)) doc.abbreviationRefs.set(refName, []);
194+
doc.abbreviationRefs.get(refName)!.push(range);
195+
}
185196
continue;
186197
}
187198

@@ -232,10 +243,17 @@ export function parseFlexDocument(text: string): FlexDocument {
232243
if (pendingScHeader !== null) {
233244
const closeIdx = trimmed.indexOf('>');
234245
if (closeIdx >= 0) {
235-
// Collect any additional SC names before the >
246+
// Collect any additional SC names before the > and record their refs on this line
236247
const before = trimmed.substring(0, closeIdx);
237248
const moreConds = before.match(/[A-Za-z_][A-Za-z0-9_]*/g);
238-
if (moreConds) pendingScHeader += ',' + moreConds.join(',');
249+
if (moreConds) {
250+
for (const cond of moreConds) {
251+
const col = line.indexOf(cond);
252+
if (!doc.startConditionRefs.has(cond)) doc.startConditionRefs.set(cond, []);
253+
doc.startConditionRefs.get(cond)!.push(Range.create(i, col >= 0 ? col : 0, i, (col >= 0 ? col : 0) + cond.length));
254+
}
255+
pendingScHeader += ',' + moreConds.join(',');
256+
}
239257
const conds = pendingScHeader.replace(/^,+/, '').split(',').filter(s => s.length > 0);
240258
pendingScHeader = null;
241259
// Expect '{' right after '>' to open the SC block
@@ -245,9 +263,16 @@ export function parseFlexDocument(text: string): FlexDocument {
245263
// actionDepth stays 0; the { is the SC block opening, not an action block
246264
}
247265
} else {
248-
// Still accumulating conditions from this line
266+
// Still accumulating conditions from this line — record their refs
249267
const moreConds = trimmed.match(/[A-Za-z_][A-Za-z0-9_]*/g);
250-
if (moreConds) pendingScHeader += ',' + moreConds.join(',');
268+
if (moreConds) {
269+
for (const cond of moreConds) {
270+
const col = line.indexOf(cond);
271+
if (!doc.startConditionRefs.has(cond)) doc.startConditionRefs.set(cond, []);
272+
doc.startConditionRefs.get(cond)!.push(Range.create(i, col >= 0 ? col : 0, i, (col >= 0 ? col : 0) + cond.length));
273+
}
274+
pendingScHeader += ',' + moreConds.join(',');
275+
}
251276
}
252277
continue;
253278
}
@@ -295,7 +320,13 @@ export function parseFlexDocument(text: string): FlexDocument {
295320
// Multi-line header start: <SC1, (no closing > on this line)
296321
const scMultiStart = trimmed.match(/^<([A-Za-z_][A-Za-z0-9_]*(?:,[A-Za-z_][A-Za-z0-9_]*)*,\s*)$/);
297322
if (scMultiStart) {
298-
pendingScHeader = scMultiStart[1].replace(/,\s*$/, '');
323+
const firstConds = scMultiStart[1].replace(/,\s*$/, '').split(',').filter(s => s.length > 0);
324+
for (const cond of firstConds) {
325+
const col = line.indexOf(cond, line.indexOf('<'));
326+
if (!doc.startConditionRefs.has(cond)) doc.startConditionRefs.set(cond, []);
327+
doc.startConditionRefs.get(cond)!.push(Range.create(i, col >= 0 ? col : 0, i, (col >= 0 ? col : 0) + cond.length));
328+
}
329+
pendingScHeader = firstConds.join(',');
299330
continue;
300331
}
301332
}
@@ -319,13 +350,15 @@ export function parseFlexDocument(text: string): FlexDocument {
319350
// ── Extract abbreviation references: {name} (but not C code {}) ───────────
320351
// Only match {name} where name is a valid identifier
321352
const abbrRefs = line.matchAll(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g);
353+
// To find the action opener '{', first strip all {name} abbreviation refs so that
354+
// the leading whitespace before {name} on an indented rule line does not confuse
355+
// the \s+{ search into matching the abbreviation's own '{' instead of the action '{'.
356+
const strippedForAction = line.replace(/\{[a-zA-Z_][a-zA-Z0-9_]*\}/g, m => ' '.repeat(m.length));
357+
const actionMatch = strippedForAction.match(/\s+\{/);
358+
const actionStart = actionMatch !== null ? (actionMatch.index! + actionMatch[0].length - 1) : line.length;
322359
for (const m of abbrRefs) {
323360
const name = m[1];
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;
361+
// Only count as abbreviation ref if it appears before the action block on this line.
329362
if (m.index !== undefined && m.index < actionStart) {
330363
const col = m.index;
331364
const range = Range.create(i, col, i, col + m[0].length);

server/src/providers/codeLens.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { DocumentModel, BisonDocument, FlexDocument, isBisonDocument } from '../
33

44
/**
55
* Code Lenses:
6-
* Bison rules → "N references" + "⬤ entry point" (start symbol only)
7-
* Flex SC decls → "N references"
6+
* Bison rules → "N references" + "⬤ entry point" (start symbol only)
7+
* Flex SC decls → "N references"
8+
* Flex abbreviations → "N references"
89
*
910
* The "N references" lens triggers `bisonFlex.showReferences` (registered
1011
* client-side) which calls `editor.action.showReferences` with pre-built args.
@@ -71,5 +72,21 @@ function getFlexCodeLenses(doc: FlexDocument, uri: string): CodeLens[] {
7172
});
7273
}
7374

75+
for (const [name, abbr] of doc.abbreviations) {
76+
const line = abbr.location.start.line;
77+
const lensRange = Range.create(line, 0, line, 0);
78+
const refCount = doc.abbreviationRefs.get(name)?.length ?? 0;
79+
80+
lenses.push({
81+
range: lensRange,
82+
command: Command.create(
83+
`$(references) ${refCount} reference${refCount !== 1 ? 's' : ''}`,
84+
'bisonFlex.showReferences',
85+
uri,
86+
{ line, character: abbr.location.start.character },
87+
),
88+
});
89+
}
90+
7491
return lenses;
7592
}

server/src/providers/diagnostics.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -879,11 +879,16 @@ function getLiteralKeyword(pat: string): string | null {
879879
* i.e., could match arbitrary letter sequences including keywords.
880880
*/
881881
function isWordPattern(pat: string): boolean {
882-
// Character class starting with a letter or underscore range: [a-z...], [A-Z...], [_...]
883-
if (/^\[[a-zA-Z_]/.test(pat)) return true;
884-
// POSIX character-class expressions that match letter sequences: [[:alpha:]], [[:alnum:]], etc.
882+
// POSIX character-class expressions that match letter sequences (e.g., [[:alpha:]],
883+
// [[:alnum:]], or chains like [[:alpha:]][[:alnum:]_]*). A prefix check is sufficient
884+
// because these patterns cannot embed mandatory non-letter components.
885885
if (/^\[\[:(alpha|upper|lower|alnum|word):\]/.test(pat)) return true;
886+
// Simple character-class patterns (e.g., [A-Z_]+ or [a-z][a-z0-9]*): only match when
887+
// the ENTIRE pattern consists of character-class groups. Patterns with mandatory
888+
// non-class suffixes — e.g., [A-Z]+(\.[A-Z]+)+ which requires a literal dot —
889+
// cannot match plain keyword strings and must not trigger the shadowing warning.
890+
if (/^\[[a-zA-Z_]/.test(pat) && /^(\[(?:[^\]\\]|\\.)*\][+*?]?)+$/.test(pat)) return true;
886891
// Common abbreviation references for identifiers
887-
if (/^\{(id|identifier|ident|IDENT|word|alpha)\}/.test(pat)) return true;
892+
if (/^\{(id|identifier|ident|IDENT|word|alpha)\}$/.test(pat)) return true;
888893
return false;
889894
}

tests/test-diagnostic-codes.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,152 @@ console.log('\n=== TEST: Flex audit checks ===');
505505
assert(unused.length === 0, 'Bug-D: abbreviation used in pattern before single-tab action must not produce flex/unused-abbrev');
506506
}
507507

508+
// ─────────────────────────────────────────────────────────────────────────────
509+
// Issue #38 — SC refs in multi-line lists / abbrev refs on indented rule lines
510+
// ─────────────────────────────────────────────────────────────────────────────
511+
console.log('\n=== TEST: Issue #38 — SC refs and abbrev refs ===');
512+
513+
{
514+
// Multi-line SC block header: all SCs must be recorded in startConditionRefs
515+
const src = [
516+
'%option noyywrap',
517+
'%x SC_A SC_B SC_C',
518+
'%%',
519+
'<SC_A,',
520+
'SC_B,',
521+
'SC_C>{',
522+
' [a-z]+ { return 1; }',
523+
'}',
524+
'%%',
525+
].join('\n');
526+
const doc = require('../server/src/parser/flexParser').parseFlexDocument(src);
527+
assert(doc.startConditionRefs.has('SC_A') && doc.startConditionRefs.get('SC_A').length >= 1,
528+
'#38: SC_A in multi-line block header recorded in startConditionRefs');
529+
assert(doc.startConditionRefs.has('SC_B') && doc.startConditionRefs.get('SC_B').length >= 1,
530+
'#38: SC_B in multi-line block header recorded in startConditionRefs');
531+
assert(doc.startConditionRefs.has('SC_C') && doc.startConditionRefs.get('SC_C').length >= 1,
532+
'#38: SC_C (on resolution line) in multi-line block header recorded in startConditionRefs');
533+
const diags = computeFlexDiagnostics(doc, src);
534+
const unusedSC = diags.filter(d => d.code === 'flex/unused-sc');
535+
assert(unusedSC.length === 0, '#38: no false flex/unused-sc for SCs in multi-line block header');
536+
}
537+
538+
{
539+
// Abbrev ref on indented rule line must be counted even with leading whitespace
540+
const src = [
541+
'%option noyywrap',
542+
'DIGIT [0-9]+',
543+
'%x ST',
544+
'%%',
545+
'<ST>{',
546+
' {DIGIT} { return 1; }',
547+
'}',
548+
'%%',
549+
].join('\n');
550+
const doc = require('../server/src/parser/flexParser').parseFlexDocument(src);
551+
assert(doc.abbreviationRefs.has('DIGIT') && doc.abbreviationRefs.get('DIGIT').length >= 1,
552+
'#38: {DIGIT} on indented rule line inside SC block recorded in abbreviationRefs');
553+
const diags = computeFlexDiagnostics(doc, src);
554+
const unusedAbbr = diags.filter(d => d.code === 'flex/unused-abbrev');
555+
assert(unusedAbbr.length === 0, '#38: no false flex/unused-abbrev for abbrev used on indented rule line');
556+
}
557+
558+
// ─────────────────────────────────────────────────────────────────────────────
559+
// Transitive abbreviation references (pplex.l scenario)
560+
// ─────────────────────────────────────────────────────────────────────────────
561+
console.log('\n=== TEST: Transitive abbreviation references ===');
562+
563+
{
564+
// ALNUM_Q and ALNUM_A are used inside ALNUM's definition, not directly in rules.
565+
// They must be counted as referenced so no flex/unused-abbrev is produced.
566+
const src = [
567+
'%option noyywrap',
568+
'ALNUM_Q\t"(\\"[^\\n])*\\""',
569+
'ALNUM_A\t"(\\\'[^\\n])*\\\'"',
570+
'ALNUM\t{ALNUM_Q}|{ALNUM_A}',
571+
'%%',
572+
'{ALNUM}\t{ return 1; }',
573+
'%%',
574+
].join('\n');
575+
const doc = require('../server/src/parser/flexParser').parseFlexDocument(src);
576+
assert(doc.abbreviationRefs.has('ALNUM_Q') && doc.abbreviationRefs.get('ALNUM_Q').length >= 1,
577+
'transitive-abbrev: ALNUM_Q used in ALNUM definition recorded in abbreviationRefs');
578+
assert(doc.abbreviationRefs.has('ALNUM_A') && doc.abbreviationRefs.get('ALNUM_A').length >= 1,
579+
'transitive-abbrev: ALNUM_A used in ALNUM definition recorded in abbreviationRefs');
580+
const diags = computeFlexDiagnostics(doc, src);
581+
const unusedAbbr = diags.filter(d => d.code === 'flex/unused-abbrev');
582+
assert(unusedAbbr.length === 0, 'transitive-abbrev: no flex/unused-abbrev for abbreviations used only inside another abbreviation');
583+
}
584+
585+
// ─────────────────────────────────────────────────────────────────────────────
586+
// isWordPattern: complex patterns with mandatory groups must not shadow keywords
587+
// ─────────────────────────────────────────────────────────────────────────────
588+
console.log('\n=== TEST: isWordPattern — complex patterns do not shadow keywords ===');
589+
590+
{
591+
// [A-Z]+(\.[A-Z]+)+ requires a dot → cannot match "IN" or "OF"
592+
// No flex/unreachable-rule should be produced for those keyword rules.
593+
const src = [
594+
'%option noyywrap',
595+
'%x COPY_STATE',
596+
'%%',
597+
'<COPY_STATE>{',
598+
' [_0-9A-Z]+(\\.[_0-9A-Z]+)+\t{ return TEXT; }',
599+
' "IN"\t{ return 1; }',
600+
' "OF"\t{ return 2; }',
601+
'}',
602+
'%%',
603+
].join('\n');
604+
const doc = require('../server/src/parser/flexParser').parseFlexDocument(src);
605+
const diags = computeFlexDiagnostics(doc, src);
606+
const unreachable = diags.filter(d => d.code === 'flex/unreachable-rule');
607+
assert(unreachable.length === 0, 'isWordPattern: keyword rules after [A-Z]+(\\.[A-Z]+)+ must not produce flex/unreachable-rule');
608+
}
609+
610+
{
611+
// [A-Z_]+ IS a simple word pattern → "IF" after it should produce flex/unreachable-rule
612+
const src = [
613+
'%option noyywrap',
614+
'%%',
615+
'[A-Z_]+\t{ return WORD; }',
616+
'"IF"\t{ return IF_KW; }',
617+
'%%',
618+
].join('\n');
619+
const doc = require('../server/src/parser/flexParser').parseFlexDocument(src);
620+
const diags = computeFlexDiagnostics(doc, src);
621+
const unreachable = diags.filter(d => d.code === 'flex/unreachable-rule');
622+
assert(unreachable.length >= 1, 'isWordPattern: simple [A-Z_]+ still shadows keyword "IF" → flex/unreachable-rule expected');
623+
}
624+
625+
// ─────────────────────────────────────────────────────────────────────────────
626+
// Issue #39 — Code Lens for abbreviations
627+
// ─────────────────────────────────────────────────────────────────────────────
628+
console.log('\n=== TEST: Issue #39 — Code Lens for abbreviations ===');
629+
630+
{
631+
const { getCodeLenses } = require('../server/src/providers/codeLens');
632+
const src = [
633+
'%option noyywrap',
634+
'DIGIT [0-9]+',
635+
'WORD [a-z]+',
636+
'%%',
637+
'{DIGIT} { return 1; }',
638+
'{DIGIT} { return 2; }',
639+
'%%',
640+
].join('\n');
641+
const doc = require('../server/src/parser/flexParser').parseFlexDocument(src);
642+
const lenses = getCodeLenses(doc, 'file:///test.l');
643+
644+
// DIGIT is on line 1, WORD on line 2
645+
const digitLens = lenses.find((l: any) => l.range.start.line === 1);
646+
const wordLens = lenses.find((l: any) => l.range.start.line === 2);
647+
648+
assert(!!digitLens, '#39: Code Lens produced for DIGIT abbreviation');
649+
assert(digitLens?.command?.title?.includes('2'), '#39: DIGIT lens shows 2 references (used twice in rules)');
650+
assert(!!wordLens, '#39: Code Lens produced for WORD abbreviation');
651+
assert(wordLens?.command?.title?.includes('0'), '#39: WORD lens shows 0 references (unused)');
652+
}
653+
508654
// ─────────────────────────────────────────────────────────────────────────────
509655
// Bison audit checks
510656
// ─────────────────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)