Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add implicit ID rule call for cross refs to getAllReachableRules #1152

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions packages/langium/src/utils/grammar-util.ts
Original file line number Diff line number Diff line change
@@ -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.
******************************************************************************/
Expand Down Expand Up @@ -47,33 +47,49 @@ export function getHiddenRules(grammar: ast.Grammar) {
* this function returns all rules of the specified grammar.
*/
export function getAllReachableRules(grammar: ast.Grammar, allTerminals: boolean): Set<ast.AbstractRule> {
const ruleNames = new Set<string>();
const entryRule = getEntryRule(grammar);
if (!entryRule) {
return new Set(grammar.rules);
}

const topMostRules = [entryRule as ast.AbstractRule].concat(getHiddenRules(grammar));
const collectedRules = new Set<ast.AbstractRule>();
for (const rule of topMostRules) {
ruleDfs(rule, ruleNames, allTerminals);
ruleDfs(rule, collectedRules, allTerminals);
}

const rules = new Set<ast.AbstractRule>();
for (const rule of grammar.rules) {
if (ruleNames.has(rule.name) || (ast.isTerminalRule(rule) && rule.hidden)) {
if (collectedRules.has(rule) || ((ast.isTerminalRule(rule) && rule.hidden))) {
rules.add(rule);
}
}
for (const rule of collectedRules) {
if (!rules.has(rule)) {
rules.add(rule);
}
}

return rules;
}

function ruleDfs(rule: ast.AbstractRule, visitedSet: Set<string>, allTerminals: boolean): void {
visitedSet.add(rule.name);
function ruleDfs(rule: ast.AbstractRule, visitedRules: Set<ast.AbstractRule> , 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);
}
}
}
}
});
Expand Down
52 changes: 51 additions & 1 deletion packages/langium/test/utils/grammar-util.test.ts
Original file line number Diff line number Diff line change
@@ -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.
******************************************************************************/
Expand All @@ -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<Grammar>(services.grammar);
Expand Down Expand Up @@ -36,4 +37,53 @@ 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(`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this proper setup to have 2 grammars reference each other?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that works fine 👍

grammar G1
entry A:
'A' name=ID;
Uncalled: name=IDX;
Called: name=INT;
terminal INT returns number: /[0-9]+/;
terminal ID: /[A-Z][\\w_]*/;
terminal IDX: /[a-z][\\w_]*/;
`);
const grammar2 = await parse(`
grammar G2
import './${Utils.basename(grammar1.uri)}'
entry B: ref=[A] s=STRING c=Called;
terminal STRING: /"(\\.|[^"\\])*"|'(\\.|[^'\\])*'/;
`);
await services.shared.workspace.DocumentBuilder.build([grammar2, grammar1]);
// act
const reachableRules = [...getAllReachableRules(grammar2.parseResult.value, true)].map(r => r.name);
// assert
expect(reachableRules).toEqual(['B', 'STRING', 'ID', 'Called', 'INT' ]);
});

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 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');
});

});