From 064a1568a2ca4610257c69bb4dd81274f9c63424 Mon Sep 17 00:00:00 2001 From: Michael Ficarra Date: Tue, 7 Apr 2026 22:38:36 -0600 Subject: [PATCH 1/3] reject AO headers with duplicate parameter names --- src/header-parser.ts | 13 +++++++++++-- src/lint/rules/variable-use-def.ts | 3 +++ test/lint.ts | 24 ++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/header-parser.ts b/src/header-parser.ts index 9d97f6be..c2425219 100644 --- a/src/header-parser.ts +++ b/src/header-parser.ts @@ -105,6 +105,13 @@ export function parseHeader(headerText: string): ParsedHeaderOrFailure { let type: 'single-line' | 'multi-line'; const params: Param[] = []; const optionalParams: Param[] = []; + + function checkDuplicateParam(paramName: string, paramNameOffset: number) { + if (params.some(p => p.name === paramName) || optionalParams.some(p => p.name === paramName)) { + errors.push({ message: `duplicate parameter ${JSON.stringify(paramName)}`, offset: paramNameOffset }); + } + } + if (text[0] === '\n') { // multiline: parse for parameter types type = 'multi-line'; @@ -142,8 +149,9 @@ export function parseHeader(headerText: string): ParsedHeaderOrFailure { errors.push({ message: 'expected parameter name', offset }); return { type: 'failure', errors }; } - offset += match[0].length; const paramName = match[0].trimRight(); + checkDuplicateParam(paramName, offset); + offset += match[0].length; ({ match, text } = eat(text, /^:+ */i)); if (!match) { @@ -216,8 +224,9 @@ export function parseHeader(headerText: string): ParsedHeaderOrFailure { errors.push({ message: 'expected parameter name', offset }); return { type: 'failure', errors }; } - offset += match[0].length; const paramName = match[0].trimRight(); + checkDuplicateParam(paramName, offset); + offset += match[0].length; (optional ? optionalParams : params).push({ name: paramName, diff --git a/src/lint/rules/variable-use-def.ts b/src/lint/rules/variable-use-def.ts index abb7b21b..fae76a07 100644 --- a/src/lint/rules/variable-use-def.ts +++ b/src/lint/rules/variable-use-def.ts @@ -144,8 +144,11 @@ export function checkVariableUsage( preceding.textContent != null ) { const isParameter = preceding.nodeName === 'H1'; + const seen = new Set(); // `__` is for _x__y_, which has textContent `_x__y_` for (const name of preceding.textContent.matchAll(/(?<=\b|_)_([a-zA-Z0-9]+)_(?=\b|_)/g)) { + if (seen.has(name[1])) continue; + seen.add(name[1]); scope.declare(name[1], null, isParameter ? 'parameter' : undefined, !isParameter); } } diff --git a/test/lint.ts b/test/lint.ts index e3055a5f..da1069b4 100644 --- a/test/lint.ts +++ b/test/lint.ts @@ -424,6 +424,30 @@ describe('linting whole program', () => { `); }); + + it('duplicate parameters', async () => { + await assertLint( + positioned` + +

+ Example ( + _x_: ~foo~, + ${M}_x_: ~bar~, + ): ~foo~ or ~bar~ +

+
+ + 1. Return _x_. + +
+ `, + { + ruleId: 'header-format', + nodeType: 'h1', + message: 'duplicate parameter "_x_"', + }, + ); + }); }); describe('closing tags', () => { From 30e37e30eeb263aa28c4cc675714b98c23ac8df7 Mon Sep 17 00:00:00 2001 From: Michael Ficarra Date: Tue, 7 Apr 2026 23:59:41 -0600 Subject: [PATCH 2/3] fix lint --- src/header-parser.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/header-parser.ts b/src/header-parser.ts index c2425219..9d790534 100644 --- a/src/header-parser.ts +++ b/src/header-parser.ts @@ -108,7 +108,10 @@ export function parseHeader(headerText: string): ParsedHeaderOrFailure { function checkDuplicateParam(paramName: string, paramNameOffset: number) { if (params.some(p => p.name === paramName) || optionalParams.some(p => p.name === paramName)) { - errors.push({ message: `duplicate parameter ${JSON.stringify(paramName)}`, offset: paramNameOffset }); + errors.push({ + message: `duplicate parameter ${JSON.stringify(paramName)}`, + offset: paramNameOffset, + }); } } From fb3b6272bce7491090ed13ab74792ff5f3e2bb1f Mon Sep 17 00:00:00 2001 From: Kevin Gibbons Date: Wed, 8 Apr 2026 09:48:54 -0700 Subject: [PATCH 3/3] add comment --- src/lint/rules/variable-use-def.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lint/rules/variable-use-def.ts b/src/lint/rules/variable-use-def.ts index fae76a07..6f67a00b 100644 --- a/src/lint/rules/variable-use-def.ts +++ b/src/lint/rules/variable-use-def.ts @@ -147,6 +147,7 @@ export function checkVariableUsage( const seen = new Set(); // `__` is for _x__y_, which has textContent `_x__y_` for (const name of preceding.textContent.matchAll(/(?<=\b|_)_([a-zA-Z0-9]+)_(?=\b|_)/g)) { + // We avoid dealing with parameter re-declaration here because tracking location information is annoying. It's handled in `checkDuplicateParam` in header-parser instead. if (seen.has(name[1])) continue; seen.add(name[1]); scope.declare(name[1], null, isParameter ? 'parameter' : undefined, !isParameter);