@@ -1889,6 +1889,149 @@ console.log('\n\n=== TEST: $n bounds — literals counted as symbols ===\n');
18891889 `$3 out-of-bounds diagnostic has Error severity` ) ;
18901890}
18911891
1892+ // ════════════════════════════════════════
1893+ // TEST: Lowercase / mixed-case token names (issue #5)
1894+ // ════════════════════════════════════════
1895+ console . log ( '\n=== TEST: Lowercase and mixed-case token names (issue #5) ===\n' ) ;
1896+
1897+ {
1898+ // Each %token declaration with a lowercase/mixed name should produce exactly one token entry.
1899+ const cases : Array < { line : string ; name : string } > = [
1900+ { line : '%token STANDARD_202x "STANDARD-202x"' , name : 'STANDARD_202x' } ,
1901+ { line : '%token lower_case_tok "lower"' , name : 'lower_case_tok' } ,
1902+ { line : '%token MIXEDcase123 "mixed"' , name : 'MIXEDcase123' } ,
1903+ { line : '%token A_1_B_2_C "alias"' , name : 'A_1_B_2_C' } ,
1904+ ] ;
1905+
1906+ for ( const { line, name } of cases ) {
1907+ const src = [ line , '%%' , 'start : ;' , '%%' ] . join ( '\n' ) ;
1908+ const doc = parseBisonDocument ( src ) ;
1909+ assert ( doc . tokens . has ( name ) ,
1910+ `parseTokenNames: '${ line } ' → token '${ name } ' is registered` ) ;
1911+ assert ( doc . tokens . size === 1 ,
1912+ `parseTokenNames: '${ line } ' → exactly 1 token (got ${ doc . tokens . size } : ${ [ ...doc . tokens . keys ( ) ] . join ( ', ' ) } )` ) ;
1913+ }
1914+
1915+ // When those tokens are USED in rules → 0 "unused token" warnings.
1916+ const usedSrc = [
1917+ '%token STANDARD_202x "STANDARD-202x"' ,
1918+ '%token lower_case_tok "lower"' ,
1919+ '%token MIXEDcase123 "mixed"' ,
1920+ '%token A_1_B_2_C "alias"' ,
1921+ '%%' ,
1922+ 'start : STANDARD_202x lower_case_tok MIXEDcase123 A_1_B_2_C ;' ,
1923+ '%%' ,
1924+ ] . join ( '\n' ) ;
1925+ const usedDoc = parseBisonDocument ( usedSrc ) ;
1926+ const usedDiags = computeBisonDiagnostics ( usedDoc , usedSrc ) ;
1927+ const unusedWarnings = usedDiags . filter ( d => d . message . includes ( 'declared but never used' ) ) ;
1928+ assert ( unusedWarnings . length === 0 ,
1929+ `All four mixed-case tokens used in rules → 0 "unused" warnings (got ${ unusedWarnings . length } : ${ unusedWarnings . map ( d => d . message ) . join ( '; ' ) } )` ) ;
1930+
1931+ // When those tokens are NOT used → exactly 1 warning each, with the full token name.
1932+ const unusedSrc = [
1933+ '%token STANDARD_202x "STANDARD-202x"' ,
1934+ '%token lower_case_tok "lower"' ,
1935+ '%token MIXEDcase123 "mixed"' ,
1936+ '%token A_1_B_2_C "alias"' ,
1937+ '%%' ,
1938+ 'start : ;' ,
1939+ '%%' ,
1940+ ] . join ( '\n' ) ;
1941+ const unusedDoc = parseBisonDocument ( unusedSrc ) ;
1942+ const unusedDiags = computeBisonDiagnostics ( unusedDoc , unusedSrc ) ;
1943+ const allUnused = unusedDiags . filter ( d => d . message . includes ( 'declared with %token but never used' ) ) ;
1944+ assert ( allUnused . length === 4 ,
1945+ `Four unused mixed-case tokens → exactly 4 warnings (got ${ allUnused . length } : ${ allUnused . map ( d => d . message ) . join ( '; ' ) } )` ) ;
1946+ for ( const name of [ 'STANDARD_202x' , 'lower_case_tok' , 'MIXEDcase123' , 'A_1_B_2_C' ] ) {
1947+ const w = allUnused . find ( d => d . message . includes ( `'${ name } '` ) ) ;
1948+ assert ( w !== undefined ,
1949+ `Unused token warning for '${ name } ' uses the full name (not a fragment)` ) ;
1950+ }
1951+ }
1952+
1953+ // ════════════════════════════════════════
1954+ // TEST: Comments in rules ignored (issue #8)
1955+ // ════════════════════════════════════════
1956+ console . log ( '\n=== TEST: Comments in rules ignored (issue #8) ===\n' ) ;
1957+
1958+ {
1959+ // Test 1: Inline /* */ block comment — token inside must NOT be flagged
1960+ const src1 = [
1961+ '%token TOKEN_A TOKEN_B' ,
1962+ '%%' ,
1963+ 'start : rule ;' ,
1964+ 'rule : TOKEN_A /* FAKE_COMMENT_TOKEN */ TOKEN_B { $$ = $1; }' ,
1965+ '%%' ,
1966+ ] . join ( '\n' ) ;
1967+ const doc1 = parseBisonDocument ( src1 ) ;
1968+ const diags1 = computeBisonDiagnostics ( doc1 , src1 ) ;
1969+ const fake1 = diags1 . filter ( d => d . message . includes ( 'FAKE_COMMENT_TOKEN' ) ) ;
1970+ assert ( fake1 . length === 0 ,
1971+ `FAKE_COMMENT_TOKEN inside /* */ must NOT be flagged (got ${ fake1 . length } diag(s): ${ fake1 . map ( d => d . message ) . join ( '; ' ) } )` ) ;
1972+
1973+ // Test 2: // line comment — token inside must NOT be flagged
1974+ const src2 = [
1975+ '%token TOKEN_C' ,
1976+ '%%' ,
1977+ 'start : rule ;' ,
1978+ 'rule : TOKEN_C // ANOTHER_FAKE_TOKEN' ,
1979+ ' { }' ,
1980+ '%%' ,
1981+ ] . join ( '\n' ) ;
1982+ const doc2 = parseBisonDocument ( src2 ) ;
1983+ const diags2 = computeBisonDiagnostics ( doc2 , src2 ) ;
1984+ const fake2 = diags2 . filter ( d => d . message . includes ( 'ANOTHER_FAKE_TOKEN' ) ) ;
1985+ assert ( fake2 . length === 0 ,
1986+ `ANOTHER_FAKE_TOKEN after // must NOT be flagged (got ${ fake2 . length } diag(s))` ) ;
1987+
1988+ // Test 3: Action block { } — identifier inside must NOT be flagged
1989+ const src3 = [
1990+ '%token TOKEN_D' ,
1991+ '%%' ,
1992+ 'start : rule ;' ,
1993+ 'rule : TOKEN_D { int x = LOOKS_LIKE_TOKEN; }' ,
1994+ '%%' ,
1995+ ] . join ( '\n' ) ;
1996+ const doc3 = parseBisonDocument ( src3 ) ;
1997+ const diags3 = computeBisonDiagnostics ( doc3 , src3 ) ;
1998+ const fake3 = diags3 . filter ( d => d . message . includes ( 'LOOKS_LIKE_TOKEN' ) ) ;
1999+ assert ( fake3 . length === 0 ,
2000+ `LOOKS_LIKE_TOKEN inside action block { } must NOT be flagged (got ${ fake3 . length } diag(s))` ) ;
2001+
2002+ // Test 4: Multi-line /* */ block comment — tokens inside must NOT be flagged
2003+ const src4 = [
2004+ '%token TOKEN_E TOKEN_F' ,
2005+ '%%' ,
2006+ 'start : rule ;' ,
2007+ 'rule : TOKEN_E /* FAKE_1' ,
2008+ ' FAKE_2 */ TOKEN_F' ,
2009+ '%%' ,
2010+ ] . join ( '\n' ) ;
2011+ const doc4 = parseBisonDocument ( src4 ) ;
2012+ const diags4 = computeBisonDiagnostics ( doc4 , src4 ) ;
2013+ const fake4a = diags4 . filter ( d => d . message . includes ( 'FAKE_1' ) ) ;
2014+ const fake4b = diags4 . filter ( d => d . message . includes ( 'FAKE_2' ) ) ;
2015+ assert ( fake4a . length === 0 ,
2016+ `FAKE_1 inside multi-line /* */ must NOT be flagged (got ${ fake4a . length } diag(s))` ) ;
2017+ assert ( fake4b . length === 0 ,
2018+ `FAKE_2 inside multi-line /* */ must NOT be flagged (got ${ fake4b . length } diag(s))` ) ;
2019+
2020+ // Test 5: Real undeclared token (not in a comment or action) MUST be flagged
2021+ const src5 = [
2022+ '%token TOKEN_G' ,
2023+ '%%' ,
2024+ 'start : rule ;' ,
2025+ 'rule : TOKEN_G UNDECLARED_TOKEN' ,
2026+ '%%' ,
2027+ ] . join ( '\n' ) ;
2028+ const doc5 = parseBisonDocument ( src5 ) ;
2029+ const diags5 = computeBisonDiagnostics ( doc5 , src5 ) ;
2030+ const undeclared5 = diags5 . filter ( d => d . message . includes ( 'UNDECLARED_TOKEN' ) ) ;
2031+ assert ( undeclared5 . length >= 1 ,
2032+ `UNDECLARED_TOKEN (not in /* */ or {}) MUST be flagged (got ${ undeclared5 . length } diag(s))` ) ;
2033+ }
2034+
18922035// ════════════════════════════════════════
18932036// SUMMARY
18942037// ════════════════════════════════════════
0 commit comments