Skip to content

Commit 8d48d21

Browse files
reject AO headers with duplicate parameter names (#689)
1 parent a79d3a3 commit 8d48d21

3 files changed

Lines changed: 42 additions & 2 deletions

File tree

src/header-parser.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ export function parseHeader(headerText: string): ParsedHeaderOrFailure {
105105
let type: 'single-line' | 'multi-line';
106106
const params: Param[] = [];
107107
const optionalParams: Param[] = [];
108+
109+
function checkDuplicateParam(paramName: string, paramNameOffset: number) {
110+
if (params.some(p => p.name === paramName) || optionalParams.some(p => p.name === paramName)) {
111+
errors.push({
112+
message: `duplicate parameter ${JSON.stringify(paramName)}`,
113+
offset: paramNameOffset,
114+
});
115+
}
116+
}
117+
108118
if (text[0] === '\n') {
109119
// multiline: parse for parameter types
110120
type = 'multi-line';
@@ -142,8 +152,9 @@ export function parseHeader(headerText: string): ParsedHeaderOrFailure {
142152
errors.push({ message: 'expected parameter name', offset });
143153
return { type: 'failure', errors };
144154
}
145-
offset += match[0].length;
146155
const paramName = match[0].trimRight();
156+
checkDuplicateParam(paramName, offset);
157+
offset += match[0].length;
147158

148159
({ match, text } = eat(text, /^:+ */i));
149160
if (!match) {
@@ -216,8 +227,9 @@ export function parseHeader(headerText: string): ParsedHeaderOrFailure {
216227
errors.push({ message: 'expected parameter name', offset });
217228
return { type: 'failure', errors };
218229
}
219-
offset += match[0].length;
220230
const paramName = match[0].trimRight();
231+
checkDuplicateParam(paramName, offset);
232+
offset += match[0].length;
221233

222234
(optional ? optionalParams : params).push({
223235
name: paramName,

src/lint/rules/variable-use-def.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,12 @@ export function checkVariableUsage(
144144
preceding.textContent != null
145145
) {
146146
const isParameter = preceding.nodeName === 'H1';
147+
const seen = new Set<string>();
147148
// `__` is for <del>_x_</del><ins>_y_</ins>, which has textContent `_x__y_`
148149
for (const name of preceding.textContent.matchAll(/(?<=\b|_)_([a-zA-Z0-9]+)_(?=\b|_)/g)) {
150+
// We avoid dealing with parameter re-declaration here because tracking location information is annoying. It's handled in `checkDuplicateParam` in header-parser instead.
151+
if (seen.has(name[1])) continue;
152+
seen.add(name[1]);
149153
scope.declare(name[1], null, isParameter ? 'parameter' : undefined, !isParameter);
150154
}
151155
}

test/lint.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,30 @@ describe('linting whole program', () => {
424424
</emu-clause>
425425
`);
426426
});
427+
428+
it('duplicate parameters', async () => {
429+
await assertLint(
430+
positioned`
431+
<emu-clause id="example" type="abstract operation">
432+
<h1>
433+
Example (
434+
_x_: ~foo~,
435+
${M}_x_: ~bar~,
436+
): ~foo~ or ~bar~
437+
</h1>
438+
<dl class="header"></dl>
439+
<emu-alg>
440+
1. Return _x_.
441+
</emu-alg>
442+
</emu-clause>
443+
`,
444+
{
445+
ruleId: 'header-format',
446+
nodeType: 'h1',
447+
message: 'duplicate parameter "_x_"',
448+
},
449+
);
450+
});
427451
});
428452

429453
describe('closing tags', () => {

0 commit comments

Comments
 (0)