diff --git a/packages/langium/src/utils/grammar-util.ts b/packages/langium/src/utils/grammar-util.ts index ef018bfc0..fc9a491c5 100644 --- a/packages/langium/src/utils/grammar-util.ts +++ b/packages/langium/src/utils/grammar-util.ts @@ -1,5 +1,5 @@ /****************************************************************************** - * Copyright 2021-2022 TypeFox GmbH + * Copyright 2021-2023 TypeFox GmbH * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. ******************************************************************************/ @@ -47,33 +47,43 @@ export function getHiddenRules(grammar: ast.Grammar) { * this function returns all rules of the specified grammar. */ export function getAllReachableRules(grammar: ast.Grammar, allTerminals: boolean): Set { - const ruleNames = new Set(); const entryRule = getEntryRule(grammar); if (!entryRule) { return new Set(grammar.rules); } const topMostRules = [entryRule as ast.AbstractRule].concat(getHiddenRules(grammar)); + const rules = new Set(); for (const rule of topMostRules) { - ruleDfs(rule, ruleNames, allTerminals); + ruleDfs(rule, rules, allTerminals); } - const rules = new Set(); for (const rule of grammar.rules) { - if (ruleNames.has(rule.name) || (ast.isTerminalRule(rule) && rule.hidden)) { + if ((ast.isTerminalRule(rule) && rule.hidden)) { rules.add(rule); } } + return rules; } -function ruleDfs(rule: ast.AbstractRule, visitedSet: Set, allTerminals: boolean): void { - visitedSet.add(rule.name); +function ruleDfs(rule: ast.AbstractRule, visitedRules: Set , allTerminals: boolean): void { + visitedRules.add(rule) streamAllContents(rule).forEach(node => { if (ast.isRuleCall(node) || (allTerminals && ast.isTerminalRuleCall(node))) { const refRule = node.rule.ref; - if (refRule && !visitedSet.has(refRule.name)) { - ruleDfs(refRule, visitedSet, allTerminals); + if (refRule && !visitedRules.has(refRule)) { + ruleDfs(refRule, visitedRules, allTerminals); + } + } else if (ast.isCrossReference(node)) { + const term = getCrossReferenceTerminal(node) + if (term !== undefined) { + if (ast.isRuleCall(term) || (allTerminals && ast.isTerminalRuleCall(term))) { + const refRule = term.rule.ref; + if (refRule && !visitedRules.has(refRule)) { + ruleDfs(refRule, visitedRules, allTerminals); + } + } } } }); diff --git a/packages/langium/test/utils/grammar-util.test.ts b/packages/langium/test/utils/grammar-util.test.ts index 5a4b7b46c..075a99919 100644 --- a/packages/langium/test/utils/grammar-util.test.ts +++ b/packages/langium/test/utils/grammar-util.test.ts @@ -1,5 +1,5 @@ /****************************************************************************** - * Copyright 2022 TypeFox GmbH + * Copyright 2022-2023 TypeFox GmbH * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. ******************************************************************************/ @@ -8,6 +8,7 @@ import type { Grammar } from '../../src'; import { describe, expect, test } from 'vitest'; import { createLangiumGrammarServices, EmptyFileSystem, getAllReachableRules } from '../../src'; import { parseHelper } from '../../src/test'; +import { Utils } from 'vscode-uri'; const services = createLangiumGrammarServices(EmptyFileSystem); const parse = parseHelper(services.grammar); @@ -36,4 +37,48 @@ describe('Grammar Utils', () => { expect(reachableRules).toContain('Ws'); }); + test('getAllReachableRules should return rules referenced in cross references', async () => { + // [A] is short for [A:ID] thus the ID rule is needed by the parser and getAllReachableRules should return ID + const grammar1 = await parse(` + grammar G1 + entry A: + 'A' name=ID; + terminal ID: /[_a-zA-Z][\\w_]*/; + `); + const grammar2 = await parse(` + grammar G2 + import './${Utils.basename(grammar1.uri)}' + entry B: ref=[A]; + ID: /[a-z]+/ + `); + await services.shared.workspace.DocumentBuilder.build([grammar2, grammar1]); + // act + const reachableRules = [...getAllReachableRules(grammar2.parseResult.value, true)].map(r => r.name); + // assert + expect(reachableRules).toContain('ID'); + }); + + test('getAllReachableRules should not return unused rules', async () => { + // no implicit ID rule call in cross ref + // [A] is short for [A:ID] thus the ID rule is needed by the parser and getAllReachableRules should return ID + const grammar1 = await parse(` + grammar G1 + entry A: + 'A' name=ID; + Other: name=STRING; + terminal ID: /[_a-zA-Z][\\w_]*/; + `); + const grammar2 = await parse(` + grammar G2 + import './${Utils.basename(grammar1.uri)}' + entry B: ref=[A]; + `); + await services.shared.workspace.DocumentBuilder.build([grammar2, grammar1]); + // act + const reachableRules = [...getAllReachableRules(grammar2.parseResult.value, true)].map(r => r.name); + + // assert + expect(reachableRules).not.toContain('STRING'); + }); + });