From f648e0fce5d72e5187cc52effcfc047ba15e8ac5 Mon Sep 17 00:00:00 2001 From: jtenner Date: Thu, 26 Mar 2026 23:49:00 -0400 Subject: [PATCH 1/6] Parse and preserve parameter decorators Extend parameter AST nodes to retain decorators, including explicit-this decorators on function signatures, without forcing a broad constructor API churn. Teach both parameter parsers to accept stacked decorators on regular, rest, explicit-this, constructor-property, function-type, and parenthesized-arrow parameters. Update the AST builder to serialize parameter decorators inline so parser fixtures can round-trip the new syntax faithfully. Add a focused parser fixture covering the preserved syntax surface before deferred validation is introduced. --- src/ast.ts | 12 +- src/extra/ast.ts | 31 +++++ src/parser.ts | 131 +++++++++++++----- tests/parser/parameter-decorators.ts | 11 ++ .../parser/parameter-decorators.ts.fixture.ts | 11 ++ 5 files changed, 156 insertions(+), 40 deletions(-) create mode 100644 tests/parser/parameter-decorators.ts create mode 100644 tests/parser/parameter-decorators.ts.fixture.ts diff --git a/src/ast.ts b/src/ast.ts index 01d8e9a421..b01353ad8e 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -181,9 +181,12 @@ export abstract class Node { name: IdentifierExpression, type: TypeNode, initializer: Expression | null, - range: Range + range: Range, + decorators: DecoratorNode[] | null = null ): ParameterNode { - return new ParameterNode(parameterKind, name, type, initializer, range); + let parameter = new ParameterNode(parameterKind, name, type, initializer, range); + parameter.decorators = decorators; + return parameter; } // special @@ -926,6 +929,9 @@ export class FunctionTypeNode extends TypeNode { ) { super(NodeKind.FunctionType, isNullable, range); } + + /** Decorators on an explicit `this` parameter, if any. */ + explicitThisDecorators: DecoratorNode[] | null = null; } /** Represents a type parameter. */ @@ -971,6 +977,8 @@ export class ParameterNode extends Node { super(NodeKind.Parameter, range); } + /** Decorators, if any. */ + decorators: DecoratorNode[] | null = null; /** Implicit field declaration, if applicable. */ implicitFieldDeclaration: FieldDeclaration | null = null; /** Common flags indicating specific traits. */ diff --git a/src/extra/ast.ts b/src/extra/ast.ts index ebe9217f90..e6bcb6c982 100644 --- a/src/extra/ast.ts +++ b/src/extra/ast.ts @@ -427,6 +427,7 @@ export class ASTBuilder { sb.push(isNullable ? "((" : "("); let explicitThisType = node.explicitThisType; if (explicitThisType) { + this.serializeParameterDecorators(node.explicitThisDecorators); sb.push("this: "); this.visitTypeNode(explicitThisType); } @@ -1153,6 +1154,7 @@ export class ASTBuilder { let numParameters = parameters.length; let explicitThisType = signature.explicitThisType; if (explicitThisType) { + this.serializeParameterDecorators(signature.explicitThisDecorators); sb.push("this: "); this.visitTypeNode(explicitThisType); } @@ -1563,9 +1565,38 @@ export class ASTBuilder { indent(sb, this.indentLevel); } + serializeParameterDecorators(decorators: DecoratorNode[] | null): void { + if (decorators) { + for (let i = 0, k = decorators.length; i < k; ++i) { + this.serializeParameterDecorator(decorators[i]); + } + } + } + + private serializeParameterDecorator(node: DecoratorNode): void { + let sb = this.sb; + sb.push("@"); + this.visitNode(node.name); + let args = node.args; + if (args) { + sb.push("("); + let numArgs = args.length; + if (numArgs) { + this.visitNode(args[0]); + for (let i = 1; i < numArgs; ++i) { + sb.push(", "); + this.visitNode(args[i]); + } + } + sb.push(")"); + } + sb.push(" "); + } + serializeParameter(node: ParameterNode): void { let sb = this.sb; let kind = node.parameterKind; + this.serializeParameterDecorators(node.decorators); let implicitFieldDeclaration = node.implicitFieldDeclaration; if (implicitFieldDeclaration) { this.serializeAccessModifiers(implicitFieldDeclaration); diff --git a/src/parser.ts b/src/parser.ts index 7c69843973..30e0da9270 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -709,6 +709,7 @@ export class Parser extends DiagnosticEmitter { let startPos = tn.tokenPos; let parameters: ParameterNode[] | null = null; let thisType: NamedTypeNode | null = null; + let thisDecorators: DecoratorNode[] | null = null; let isSignature: bool = false; let firstParamNameNoType: IdentifierExpression | null = null; let firstParamKind: ParameterKind = ParameterKind.Default; @@ -723,6 +724,12 @@ export class Parser extends DiagnosticEmitter { do { let paramStart = -1; let kind = ParameterKind.Default; + let decorators = this.parseParameterDecorators(tn); + if (decorators) { + paramStart = decorators[0].range.start; + isSignature = true; + tn.discard(state); + } if (tn.skip(Token.Dot_Dot_Dot)) { paramStart = tn.tokenPos; isSignature = true; @@ -745,6 +752,7 @@ export class Parser extends DiagnosticEmitter { return null; } thisType = type; + thisDecorators = decorators; } else { tn.reset(state); this.tryParseSignatureIsSignature = false; @@ -773,7 +781,7 @@ export class Parser extends DiagnosticEmitter { this.tryParseSignatureIsSignature = isSignature; return null; } - let param = Node.createParameter(kind, name, type, null, tn.range(paramStart, tn.pos)); + let param = Node.createParameter(kind, name, type, null, tn.range(paramStart, tn.pos), decorators); if (!parameters) parameters = [ param ]; else parameters.push(param); } else { @@ -784,7 +792,7 @@ export class Parser extends DiagnosticEmitter { } } if (isSignature) { - let param = Node.createParameter(kind, name, Node.createOmittedType(tn.range(tn.pos)), null, tn.range(paramStart, tn.pos)); + let param = Node.createParameter(kind, name, Node.createOmittedType(tn.range(tn.pos)), null, tn.range(paramStart, tn.pos), decorators); if (!parameters) parameters = [ param ]; else parameters.push(param); this.error( @@ -869,13 +877,15 @@ export class Parser extends DiagnosticEmitter { if (!parameters) parameters = []; - return Node.createFunctionType( + let functionType = Node.createFunctionType( parameters, returnType, thisType, false, tn.range(startPos, tn.pos) ); + functionType.explicitThisDecorators = thisDecorators; + return functionType; } // statements @@ -924,6 +934,19 @@ export class Parser extends DiagnosticEmitter { return null; } + private parseParameterDecorators( + tn: Tokenizer + ): DecoratorNode[] | null { + let decorators: DecoratorNode[] | null = null; + while (tn.skip(Token.At)) { + let decorator = this.parseDecorator(tn); + if (!decorator) break; + if (!decorators) decorators = [decorator]; + else decorators.push(decorator); + } + return decorators; + } + parseVariable( tn: Tokenizer, flags: CommonFlags, @@ -1227,6 +1250,7 @@ export class Parser extends DiagnosticEmitter { } private parseParametersThis: NamedTypeNode | null = null; + private parseParametersThisDecorators: DecoratorNode[] | null = null; parseParameters( tn: Tokenizer, @@ -1243,40 +1267,50 @@ export class Parser extends DiagnosticEmitter { // check if there is a leading `this` parameter this.parseParametersThis = null; - if (tn.skip(Token.This)) { - if (tn.skip(Token.Colon)) { - thisType = this.parseType(tn); // reports - if (!thisType) return null; - if (thisType.kind == NodeKind.NamedType) { - this.parseParametersThis = thisType; - } else { - this.error( - DiagnosticCode.Identifier_expected, - thisType.range - ); - } - } else { - this.error( - DiagnosticCode._0_expected, - tn.range(), ":" - ); - return null; - } - if (!tn.skip(Token.Comma)) { - if (tn.skip(Token.CloseParen)) { - return parameters; + this.parseParametersThisDecorators = null; + + let first = true; + while (true) { + if (tn.skip(Token.CloseParen)) break; + + let paramDecorators = this.parseParameterDecorators(tn); + + if (first && tn.skip(Token.This)) { + if (tn.skip(Token.Colon)) { + thisType = this.parseType(tn); // reports + if (!thisType) return null; + if (thisType.kind == NodeKind.NamedType) { + this.parseParametersThis = thisType; + this.parseParametersThisDecorators = paramDecorators; + } else { + this.error( + DiagnosticCode.Identifier_expected, + thisType.range + ); + } } else { this.error( DiagnosticCode._0_expected, - tn.range(), ")" + tn.range(), ":" ); return null; } + first = false; + if (!tn.skip(Token.Comma)) { + if (tn.skip(Token.CloseParen)) { + break; + } else { + this.error( + DiagnosticCode._0_expected, + tn.range(), ")" + ); + return null; + } + } + continue; } - } - while (!tn.skip(Token.CloseParen)) { - let param = this.parseParameter(tn, isConstructor); // reports + let param = this.parseParameter(tn, isConstructor, paramDecorators); // reports if (!param) return null; if (seenRest && !reportedRest) { this.error( @@ -1305,6 +1339,7 @@ export class Parser extends DiagnosticEmitter { } } parameters.push(param); + first = false; if (!tn.skip(Token.Comma)) { if (tn.skip(Token.CloseParen)) { break; @@ -1322,24 +1357,27 @@ export class Parser extends DiagnosticEmitter { parseParameter( tn: Tokenizer, - isConstructor: bool = false + isConstructor: bool = false, + decorators: DecoratorNode[] | null = null ): ParameterNode | null { // before: ('public' | 'private' | 'protected' | '...')? Identifier '?'? (':' Type)? ('=' Expression)? let isRest = false; let isOptional = false; - let startRange: Range | null = null; + let startRange: Range | null = decorators + ? decorators[0].range + : null; let accessFlags: CommonFlags = CommonFlags.None; if (isConstructor) { if (tn.skip(Token.Public)) { - startRange = tn.range(); + if (!startRange) startRange = tn.range(); accessFlags |= CommonFlags.Public; } else if (tn.skip(Token.Protected)) { - startRange = tn.range(); + if (!startRange) startRange = tn.range(); accessFlags |= CommonFlags.Protected; } else if (tn.skip(Token.Private)) { - startRange = tn.range(); + if (!startRange) startRange = tn.range(); accessFlags |= CommonFlags.Private; } if (tn.peek() == Token.Readonly) { @@ -1361,12 +1399,12 @@ export class Parser extends DiagnosticEmitter { tn.range() ); } else { - startRange = tn.range(); + if (!startRange) startRange = tn.range(); } isRest = true; } if (tn.skipIdentifier()) { - if (!isRest) startRange = tn.range(); + if (!isRest && !startRange) startRange = tn.range(); let identifier = Node.createIdentifierExpression(tn.readIdentifier(), tn.range()); let type: TypeNode | null = null; if (isOptional = tn.skip(Token.Question)) { @@ -1411,7 +1449,8 @@ export class Parser extends DiagnosticEmitter { identifier, type, initializer, - Range.join(assert(startRange), tn.range()) + Range.join(assert(startRange), tn.range()), + decorators ); param.flags |= accessFlags; return param; @@ -1523,6 +1562,7 @@ export class Parser extends DiagnosticEmitter { false, tn.range(signatureStart, tn.pos) ); + signature.explicitThisDecorators = this.parseParametersThisDecorators; let body: Statement | null = null; if (tn.skip(Token.OpenBrace)) { @@ -1597,7 +1637,16 @@ export class Parser extends DiagnosticEmitter { let parameters = this.parseParameters(tn); if (!parameters) return null; - return this.parseFunctionExpressionCommon(tn, name, parameters, this.parseParametersThis, arrowKind, startPos, signatureStart); + return this.parseFunctionExpressionCommon( + tn, + name, + parameters, + this.parseParametersThis, + this.parseParametersThisDecorators, + arrowKind, + startPos, + signatureStart + ); } private parseFunctionExpressionCommon( @@ -1605,6 +1654,7 @@ export class Parser extends DiagnosticEmitter { name: IdentifierExpression, parameters: ParameterNode[], explicitThis: NamedTypeNode | null, + explicitThisDecorators: DecoratorNode[] | null, arrowKind: ArrowKind, startPos: i32 = -1, signatureStart: i32 = -1 @@ -1637,6 +1687,7 @@ export class Parser extends DiagnosticEmitter { false, tn.range(signatureStart, tn.pos) ); + signature.explicitThisDecorators = explicitThisDecorators; let body: Statement | null = null; if (arrowKind) { @@ -2282,6 +2333,7 @@ export class Parser extends DiagnosticEmitter { false, tn.range(signatureStart, tn.pos) ); + signature.explicitThisDecorators = this.parseParametersThisDecorators; let body: Statement | null = null; if (tn.skip(Token.OpenBrace)) { @@ -3729,6 +3781,7 @@ export class Parser extends DiagnosticEmitter { Node.createEmptyIdentifierExpression(tn.range(startPos)), [], null, + null, ArrowKind.Parenthesized ); } @@ -3738,6 +3791,7 @@ export class Parser extends DiagnosticEmitter { switch (tn.next(IdentifierHandling.Prefer)) { // function expression + case Token.At: case Token.Dot_Dot_Dot: { tn.reset(state); return this.parseFunctionExpression(tn); @@ -3930,6 +3984,7 @@ export class Parser extends DiagnosticEmitter { ) ], null, + null, ArrowKind.Single, startPos ); diff --git a/tests/parser/parameter-decorators.ts b/tests/parser/parameter-decorators.ts new file mode 100644 index 0000000000..4dbd1b1571 --- /dev/null +++ b/tests/parser/parameter-decorators.ts @@ -0,0 +1,11 @@ +function regular(@first @second("x") value: i32, @optional maybe?: i32): void {} +function withthis(@self this: i32, @rest ...values: i32[]): i32 { return this; } + +class Box { + constructor(@field public value: i32) {} + method(@arg value: i32): void {} +} + +type Callback = (@self this: i32, @arg value: i32, @rest ...values: i32[]) => void; +const expression = function (@arg value: i32): void {}; +const arrow = (@arg value: i32): void => {}; diff --git a/tests/parser/parameter-decorators.ts.fixture.ts b/tests/parser/parameter-decorators.ts.fixture.ts new file mode 100644 index 0000000000..8ca6033bc0 --- /dev/null +++ b/tests/parser/parameter-decorators.ts.fixture.ts @@ -0,0 +1,11 @@ +function regular(@first @second("x") value: i32, @optional maybe?: i32): void {} +function withthis(@self this: i32, @rest ...values: Array): i32 { + return this; +} +class Box { + constructor(@field public value: i32) {} + method(@arg value: i32): void {} +} +type Callback = (@self this: i32, @arg value: i32, @rest ...values: Array) => void; +const expression = function(@arg value: i32): void {}; +const arrow = (@arg value: i32): void => {}; From be3857d84b7dfc8956859f611412d67573608120 Mon Sep 17 00:00:00 2001 From: jtenner Date: Thu, 26 Mar 2026 23:53:14 -0400 Subject: [PATCH 2/6] Reject surviving parameter decorators after transforms Add a Program-owned validation pass that walks the final AST and reports TS1206 once per decorated parameter, using the full decorator-list span for the diagnostic range. Invoke that validation from compile after transforms have had their afterInitialize window, so transformers can still remove parameter decorators before any diagnostics are emitted. Add a dedicated compiler rejection fixture covering regular, rest, explicit-this, constructor-property, function-type, function-expression, and arrow-parameter cases. --- src/compiler.ts | 1 + src/program.ts | 415 +++++++++++++++++++++++ tests/compiler/parameter-decorators.json | 23 ++ tests/compiler/parameter-decorators.ts | 14 + 4 files changed, 453 insertions(+) create mode 100644 tests/compiler/parameter-decorators.json create mode 100644 tests/compiler/parameter-decorators.ts diff --git a/src/compiler.ts b/src/compiler.ts index 36f09ab8aa..8785655609 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -536,6 +536,7 @@ export class Compiler extends DiagnosticEmitter { // initialize lookup maps, built-ins, imports, exports, etc. this.program.initialize(); + this.program.validateParameterDecorators(); // Binaryen treats all function references as being leaked to the outside world when diff --git a/src/program.ts b/src/program.ts index 8bcf1bfa19..2a8b6500f7 100644 --- a/src/program.ts +++ b/src/program.ts @@ -86,29 +86,62 @@ import { ArrowKind, Expression, + ArrayLiteralExpression, + AssertionExpression, + BinaryExpression, + CallExpression, + ClassExpression, + CommaExpression, + ElementAccessExpression, + FunctionExpression, IdentifierExpression, + InstanceOfExpression, + LiteralExpression, + NewExpression, + ObjectLiteralExpression, + ParenthesizedExpression, + PropertyAccessExpression, + TernaryExpression, + TemplateLiteralExpression, + UnaryPostfixExpression, + UnaryPrefixExpression, LiteralKind, StringLiteralExpression, Statement, + BlockStatement, ClassDeclaration, DeclarationStatement, + DoStatement, + ExpressionStatement, EnumDeclaration, EnumValueDeclaration, ExportMember, ExportDefaultStatement, ExportStatement, FieldDeclaration, + ForOfStatement, + ForStatement, FunctionDeclaration, + IfStatement, ImportDeclaration, ImportStatement, + IndexSignatureNode, InterfaceDeclaration, MethodDeclaration, + ModuleDeclaration, NamespaceDeclaration, + ReturnStatement, + SwitchCase, + SwitchStatement, + ThrowStatement, + TryStatement, TypeDeclaration, VariableDeclaration, VariableLikeDeclarationStatement, VariableStatement, + VoidStatement, + WhileStatement, ParameterKind, ParameterNode, TypeName @@ -462,6 +495,8 @@ export class Program extends DiagnosticEmitter { nextSignatureId: i32 = 0; /** An indicator if the program has been initialized. */ initialized: bool = false; + /** An indicator if parameter decorators have been validated. */ + parameterDecoratorsValidated: bool = false; // Lookup maps @@ -1497,6 +1532,386 @@ export class Program extends DiagnosticEmitter { } } + /** Validates that no parameter decorators survive past transform time. */ + validateParameterDecorators(): void { + if (this.parameterDecoratorsValidated) return; + this.parameterDecoratorsValidated = true; + let sources = this.sources; + for (let i = 0, k = sources.length; i < k; ++i) { + this.validateParameterDecoratorsInSource(sources[i]); + } + } + + private validateParameterDecoratorsInSource(source: Source): void { + let statements = source.statements; + for (let i = 0, k = statements.length; i < k; ++i) { + this.validateParameterDecoratorsInStatement(statements[i]); + } + } + + private validateParameterDecoratorsInStatements(statements: Statement[] | null): void { + if (statements) { + for (let i = 0, k = statements.length; i < k; ++i) { + this.validateParameterDecoratorsInStatement(statements[i]); + } + } + } + + private validateParameterDecoratorsInStatement(statement: Statement | null): void { + if (!statement) return; + switch (statement.kind) { + case NodeKind.Block: { + this.validateParameterDecoratorsInStatements((statement).statements); + break; + } + case NodeKind.ClassDeclaration: + case NodeKind.InterfaceDeclaration: { + this.validateParameterDecoratorsInClassDeclaration(statement); + break; + } + case NodeKind.Do: { + let doStatement = statement; + this.validateParameterDecoratorsInStatement(doStatement.body); + this.validateParameterDecoratorsInExpression(doStatement.condition); + break; + } + case NodeKind.EnumDeclaration: { + this.validateParameterDecoratorsInEnumDeclaration(statement); + break; + } + case NodeKind.ExportDefault: { + this.validateParameterDecoratorsInDeclaration((statement).declaration); + break; + } + case NodeKind.Expression: { + this.validateParameterDecoratorsInExpression((statement).expression); + break; + } + case NodeKind.For: { + let forStatement = statement; + this.validateParameterDecoratorsInStatement(forStatement.initializer); + this.validateParameterDecoratorsInExpression(forStatement.condition); + this.validateParameterDecoratorsInExpression(forStatement.incrementor); + this.validateParameterDecoratorsInStatement(forStatement.body); + break; + } + case NodeKind.ForOf: { + let forOfStatement = statement; + this.validateParameterDecoratorsInStatement(forOfStatement.variable); + this.validateParameterDecoratorsInExpression(forOfStatement.iterable); + this.validateParameterDecoratorsInStatement(forOfStatement.body); + break; + } + case NodeKind.FunctionDeclaration: + case NodeKind.MethodDeclaration: { + this.validateParameterDecoratorsInFunctionDeclaration(statement); + break; + } + case NodeKind.If: { + let ifStatement = statement; + this.validateParameterDecoratorsInExpression(ifStatement.condition); + this.validateParameterDecoratorsInStatement(ifStatement.ifTrue); + this.validateParameterDecoratorsInStatement(ifStatement.ifFalse); + break; + } + case NodeKind.NamespaceDeclaration: { + this.validateParameterDecoratorsInStatements((statement).members); + break; + } + case NodeKind.Return: { + this.validateParameterDecoratorsInExpression((statement).value); + break; + } + case NodeKind.Switch: { + let switchStatement = statement; + this.validateParameterDecoratorsInExpression(switchStatement.condition); + let cases = switchStatement.cases; + for (let i = 0, k = cases.length; i < k; ++i) { + this.validateParameterDecoratorsInSwitchCase(cases[i]); + } + break; + } + case NodeKind.Throw: { + this.validateParameterDecoratorsInExpression((statement).value); + break; + } + case NodeKind.Try: { + let tryStatement = statement; + this.validateParameterDecoratorsInStatements(tryStatement.bodyStatements); + this.validateParameterDecoratorsInStatements(tryStatement.catchStatements); + this.validateParameterDecoratorsInStatements(tryStatement.finallyStatements); + break; + } + case NodeKind.TypeDeclaration: { + this.validateParameterDecoratorsInTypeDeclaration(statement); + break; + } + case NodeKind.Variable: { + let declarations = (statement).declarations; + for (let i = 0, k = declarations.length; i < k; ++i) { + this.validateParameterDecoratorsInVariableLikeDeclaration(declarations[i]); + } + break; + } + case NodeKind.Void: { + this.validateParameterDecoratorsInExpression((statement).expression); + break; + } + case NodeKind.While: { + let whileStatement = statement; + this.validateParameterDecoratorsInExpression(whileStatement.condition); + this.validateParameterDecoratorsInStatement(whileStatement.body); + break; + } + } + } + + private validateParameterDecoratorsInDeclaration(declaration: DeclarationStatement): void { + switch (declaration.kind) { + case NodeKind.ClassDeclaration: + case NodeKind.InterfaceDeclaration: { + this.validateParameterDecoratorsInClassDeclaration(declaration); + break; + } + case NodeKind.EnumDeclaration: { + this.validateParameterDecoratorsInEnumDeclaration(declaration); + break; + } + case NodeKind.FieldDeclaration: + case NodeKind.VariableDeclaration: { + this.validateParameterDecoratorsInVariableLikeDeclaration(declaration); + break; + } + case NodeKind.FunctionDeclaration: + case NodeKind.MethodDeclaration: { + this.validateParameterDecoratorsInFunctionDeclaration(declaration); + break; + } + case NodeKind.NamespaceDeclaration: { + this.validateParameterDecoratorsInStatements((declaration).members); + break; + } + case NodeKind.TypeDeclaration: { + this.validateParameterDecoratorsInTypeDeclaration(declaration); + break; + } + } + } + + private validateParameterDecoratorsInClassDeclaration(node: ClassDeclaration): void { + this.validateParameterDecoratorsInTypeParameters(node.typeParameters); + this.validateParameterDecoratorsInType(node.extendsType); + let implementsTypes = node.implementsTypes; + if (implementsTypes) { + for (let i = 0, k = implementsTypes.length; i < k; ++i) { + this.validateParameterDecoratorsInType(implementsTypes[i]); + } + } + this.validateParameterDecoratorsInIndexSignature(node.indexSignature); + let members = node.members; + for (let i = 0, k = members.length; i < k; ++i) { + this.validateParameterDecoratorsInDeclaration(members[i]); + } + } + + private validateParameterDecoratorsInEnumDeclaration(node: EnumDeclaration): void { + let values = node.values; + for (let i = 0, k = values.length; i < k; ++i) { + this.validateParameterDecoratorsInVariableLikeDeclaration(values[i]); + } + } + + private validateParameterDecoratorsInFunctionDeclaration(node: FunctionDeclaration): void { + this.validateParameterDecoratorsInTypeParameters(node.typeParameters); + this.validateParameterDecoratorsInFunctionType(node.signature); + this.validateParameterDecoratorsInStatement(node.body); + } + + private validateParameterDecoratorsInTypeDeclaration(node: TypeDeclaration): void { + this.validateParameterDecoratorsInTypeParameters(node.typeParameters); + this.validateParameterDecoratorsInType(node.type); + } + + private validateParameterDecoratorsInVariableLikeDeclaration(node: VariableLikeDeclarationStatement): void { + this.validateParameterDecoratorsInType(node.type); + this.validateParameterDecoratorsInExpression(node.initializer); + } + + private validateParameterDecoratorsInSwitchCase(node: SwitchCase): void { + this.validateParameterDecoratorsInExpression(node.label); + this.validateParameterDecoratorsInStatements(node.statements); + } + + private validateParameterDecoratorsInIndexSignature(node: IndexSignatureNode | null): void { + if (!node) return; + this.validateParameterDecoratorsInType(node.keyType); + this.validateParameterDecoratorsInType(node.valueType); + } + + private validateParameterDecoratorsInExpression(node: Expression | null): void { + if (!node) return; + switch (node.kind) { + case NodeKind.Assertion: { + let assertion = node; + this.validateParameterDecoratorsInExpression(assertion.expression); + this.validateParameterDecoratorsInType(assertion.toType); + break; + } + case NodeKind.Binary: { + let binary = node; + this.validateParameterDecoratorsInExpression(binary.left); + this.validateParameterDecoratorsInExpression(binary.right); + break; + } + case NodeKind.Call: { + let call = node; + this.validateParameterDecoratorsInExpression(call.expression); + this.validateParameterDecoratorsInTypes(call.typeArguments); + this.validateParameterDecoratorsInExpressions(call.args); + break; + } + case NodeKind.Class: { + this.validateParameterDecoratorsInClassDeclaration((node).declaration); + break; + } + case NodeKind.Comma: { + this.validateParameterDecoratorsInExpressions((node).expressions); + break; + } + case NodeKind.ElementAccess: { + let elementAccess = node; + this.validateParameterDecoratorsInExpression(elementAccess.expression); + this.validateParameterDecoratorsInExpression(elementAccess.elementExpression); + break; + } + case NodeKind.Function: { + this.validateParameterDecoratorsInFunctionDeclaration((node).declaration); + break; + } + case NodeKind.InstanceOf: { + let instanceOf = node; + this.validateParameterDecoratorsInExpression(instanceOf.expression); + this.validateParameterDecoratorsInType(instanceOf.isType); + break; + } + case NodeKind.Literal: { + this.validateParameterDecoratorsInLiteral(node); + break; + } + case NodeKind.New: { + let newExpression = node; + this.validateParameterDecoratorsInTypes(newExpression.typeArguments); + this.validateParameterDecoratorsInExpressions(newExpression.args); + break; + } + case NodeKind.Parenthesized: { + this.validateParameterDecoratorsInExpression((node).expression); + break; + } + case NodeKind.PropertyAccess: { + this.validateParameterDecoratorsInExpression((node).expression); + break; + } + case NodeKind.Ternary: { + let ternary = node; + this.validateParameterDecoratorsInExpression(ternary.condition); + this.validateParameterDecoratorsInExpression(ternary.ifThen); + this.validateParameterDecoratorsInExpression(ternary.ifElse); + break; + } + case NodeKind.UnaryPostfix: { + this.validateParameterDecoratorsInExpression((node).operand); + break; + } + case NodeKind.UnaryPrefix: { + this.validateParameterDecoratorsInExpression((node).operand); + break; + } + } + } + + private validateParameterDecoratorsInExpressions(nodes: Expression[] | null): void { + if (nodes) { + for (let i = 0, k = nodes.length; i < k; ++i) { + this.validateParameterDecoratorsInExpression(nodes[i]); + } + } + } + + private validateParameterDecoratorsInLiteral(node: LiteralExpression): void { + switch (node.literalKind) { + case LiteralKind.Array: { + this.validateParameterDecoratorsInExpressions((node).elementExpressions); + break; + } + case LiteralKind.Object: { + this.validateParameterDecoratorsInExpressions((node).values); + break; + } + case LiteralKind.Template: { + this.validateParameterDecoratorsInExpressions((node).expressions); + break; + } + } + } + + private validateParameterDecoratorsInType(node: TypeNode | null): void { + if (!node) return; + switch (node.kind) { + case NodeKind.FunctionType: { + this.validateParameterDecoratorsInFunctionType(node); + break; + } + case NodeKind.NamedType: { + this.validateParameterDecoratorsInTypes((node).typeArguments); + break; + } + } + } + + private validateParameterDecoratorsInTypes(nodes: TypeNode[] | null): void { + if (nodes) { + for (let i = 0, k = nodes.length; i < k; ++i) { + this.validateParameterDecoratorsInType(nodes[i]); + } + } + } + + private validateParameterDecoratorsInTypeParameters(nodes: TypeParameterNode[] | null): void { + if (nodes) { + for (let i = 0, k = nodes.length; i < k; ++i) { + let node = nodes[i]; + this.validateParameterDecoratorsInType(node.extendsType); + this.validateParameterDecoratorsInType(node.defaultType); + } + } + } + + private validateParameterDecoratorsInFunctionType(node: FunctionTypeNode): void { + this.reportParameterDecorators(node.explicitThisDecorators); + this.validateParameterDecoratorsInType(node.explicitThisType); + let parameters = node.parameters; + for (let i = 0, k = parameters.length; i < k; ++i) { + this.validateParameterDecoratorsInParameter(parameters[i]); + } + this.validateParameterDecoratorsInType(node.returnType); + } + + private validateParameterDecoratorsInParameter(node: ParameterNode): void { + this.reportParameterDecorators(node.decorators); + this.validateParameterDecoratorsInType(node.type); + this.validateParameterDecoratorsInExpression(node.initializer); + } + + private reportParameterDecorators(decorators: DecoratorNode[] | null): void { + if (decorators && decorators.length > 0) { + this.error( + DiagnosticCode.Decorators_are_not_valid_here, + Range.join(decorators[0].range, decorators[decorators.length - 1].range) + ); + } + } + /** Processes overridden members by this class in a base class. */ private processOverrides( thisPrototype: ClassPrototype, diff --git a/tests/compiler/parameter-decorators.json b/tests/compiler/parameter-decorators.json new file mode 100644 index 0000000000..8bae20a4ab --- /dev/null +++ b/tests/compiler/parameter-decorators.json @@ -0,0 +1,23 @@ +{ + "asc_flags": [ + ], + "stderr": [ + "TS1206: Decorators are not valid here.", + "function regular(@first value: i32): void {}", + "TS1206: Decorators are not valid here.", + "function rest(@rest ...values: i32[]): void {}", + "TS1206: Decorators are not valid here.", + "function withthis(@self this: i32, value: i32): i32 { return this; }", + "TS1206: Decorators are not valid here.", + "constructor(@field public value: i32) {}", + "TS1206: Decorators are not valid here.", + "method(@arg value: i32): void {}", + "TS1206: Decorators are not valid here.", + "type Callback = (@arg value: i32) => void;", + "TS1206: Decorators are not valid here.", + "const expression = function(@arg value: i32): void {};", + "TS1206: Decorators are not valid here.", + "const arrow = (@arg value: i32): void => {};", + "EOF" + ] +} diff --git a/tests/compiler/parameter-decorators.ts b/tests/compiler/parameter-decorators.ts new file mode 100644 index 0000000000..0a2ec5c8d0 --- /dev/null +++ b/tests/compiler/parameter-decorators.ts @@ -0,0 +1,14 @@ +function regular(@first value: i32): void {} +function rest(@rest ...values: i32[]): void {} +function withthis(@self this: i32, value: i32): i32 { return this; } + +class Box { + constructor(@field public value: i32) {} + method(@arg value: i32): void {} +} + +type Callback = (@arg value: i32) => void; +const expression = function(@arg value: i32): void {}; +const arrow = (@arg value: i32): void => {}; + +ERROR("EOF"); From 57adf26e58abf7a3b6a65f80399af1fda183256f Mon Sep 17 00:00:00 2001 From: jtenner Date: Thu, 26 Mar 2026 23:57:59 -0400 Subject: [PATCH 3/6] Cover transform-time removal of parameter decorators Add a dedicated transform input containing the same invalid parameter-decorator forms exercised by the compiler rejection fixture. Introduce ESM and CommonJS afterInitialize transforms that walk the AST and strip parameter decorators, including explicit-this decorators on function signatures. Extend the transform test scripts to compile that input with the stripping transforms, proving no TS1206 diagnostics are emitted once transforms remove the decorators in time. --- package.json | 4 +- .../cjs/remove-parameter-decorators.js | 327 ++++++++++++++++++ tests/transform/parameter-decorators.ts | 11 + .../transform/remove-parameter-decorators.js | 327 ++++++++++++++++++ 4 files changed, 667 insertions(+), 2 deletions(-) create mode 100644 tests/transform/cjs/remove-parameter-decorators.js create mode 100644 tests/transform/parameter-decorators.ts create mode 100644 tests/transform/remove-parameter-decorators.js diff --git a/package.json b/package.json index 627dc00210..da10621201 100644 --- a/package.json +++ b/package.json @@ -84,8 +84,8 @@ "test:browser": "node --enable-source-maps tests/browser", "test:asconfig": "cd tests/asconfig && npm run test", "test:transform": "npm run test:transform:esm && npm run test:transform:cjs", - "test:transform:esm": "node bin/asc tests/compiler/empty --transform ./tests/transform/index.js --noEmit && node bin/asc tests/compiler/empty --transform ./tests/transform/simple.js --noEmit", - "test:transform:cjs": "node bin/asc tests/compiler/empty --transform ./tests/transform/cjs/index.js --noEmit && node bin/asc tests/compiler/empty --transform ./tests/transform/cjs/simple.js --noEmit", + "test:transform:esm": "node bin/asc tests/compiler/empty --transform ./tests/transform/index.js --noEmit && node bin/asc tests/compiler/empty --transform ./tests/transform/simple.js --noEmit && node bin/asc tests/transform/parameter-decorators.ts --transform ./tests/transform/remove-parameter-decorators.js --noEmit", + "test:transform:cjs": "node bin/asc tests/compiler/empty --transform ./tests/transform/cjs/index.js --noEmit && node bin/asc tests/compiler/empty --transform ./tests/transform/cjs/simple.js --noEmit && node bin/asc tests/transform/parameter-decorators.ts --transform ./tests/transform/cjs/remove-parameter-decorators.js --noEmit", "test:cli": "node tests/cli/options.js", "asbuild": "npm run asbuild:debug && npm run asbuild:release", "asbuild:debug": "node bin/asc --config src/asconfig.json --target debug", diff --git a/tests/transform/cjs/remove-parameter-decorators.js b/tests/transform/cjs/remove-parameter-decorators.js new file mode 100644 index 0000000000..80168e221c --- /dev/null +++ b/tests/transform/cjs/remove-parameter-decorators.js @@ -0,0 +1,327 @@ +console.log("CommonJS parameter decorator removal transform loaded"); + +const NodeKind = { + NamedType: 1, + FunctionType: 2, + Assertion: 7, + Binary: 8, + Call: 9, + Class: 10, + Comma: 11, + ElementAccess: 12, + Function: 14, + InstanceOf: 15, + Literal: 16, + New: 17, + Parenthesized: 20, + PropertyAccess: 21, + Ternary: 22, + UnaryPostfix: 27, + UnaryPrefix: 28, + Block: 30, + Do: 33, + ExportDefault: 36, + Expression: 38, + For: 39, + ForOf: 40, + If: 41, + Return: 43, + Switch: 44, + Throw: 45, + Try: 46, + Variable: 47, + Void: 48, + While: 49, + ClassDeclaration: 51, + EnumDeclaration: 52, + FieldDeclaration: 54, + FunctionDeclaration: 55, + InterfaceDeclaration: 57, + MethodDeclaration: 58, + NamespaceDeclaration: 59, + TypeDeclaration: 60, + VariableDeclaration: 61 +}; + +const LiteralKind = { + Template: 3, + Array: 5, + Object: 6 +}; + +exports.afterInitialize = (program) => { + console.log("- afterInitialize strip parameter decorators"); + for (const source of program.sources) { + clearStatements(source.statements); + } +}; + +function clearStatements(statements) { + if (!statements) return; + for (const statement of statements) { + clearStatement(statement); + } +} + +function clearStatement(statement) { + if (!statement) return; + switch (statement.kind) { + case NodeKind.Block: + clearStatements(statement.statements); + break; + case NodeKind.ClassDeclaration: + case NodeKind.InterfaceDeclaration: + clearClassDeclaration(statement); + break; + case NodeKind.Do: + clearStatement(statement.body); + clearExpression(statement.condition); + break; + case NodeKind.EnumDeclaration: + for (const value of statement.values) { + clearVariableLike(value); + } + break; + case NodeKind.ExportDefault: + clearDeclaration(statement.declaration); + break; + case NodeKind.Expression: + clearExpression(statement.expression); + break; + case NodeKind.For: + clearStatement(statement.initializer); + clearExpression(statement.condition); + clearExpression(statement.incrementor); + clearStatement(statement.body); + break; + case NodeKind.ForOf: + clearStatement(statement.variable); + clearExpression(statement.iterable); + clearStatement(statement.body); + break; + case NodeKind.FunctionDeclaration: + case NodeKind.MethodDeclaration: + clearFunctionDeclaration(statement); + break; + case NodeKind.If: + clearExpression(statement.condition); + clearStatement(statement.ifTrue); + clearStatement(statement.ifFalse); + break; + case NodeKind.NamespaceDeclaration: + clearStatements(statement.members); + break; + case NodeKind.Return: + clearExpression(statement.value); + break; + case NodeKind.Switch: + clearExpression(statement.condition); + for (const switchCase of statement.cases) { + clearExpression(switchCase.label); + clearStatements(switchCase.statements); + } + break; + case NodeKind.Throw: + clearExpression(statement.value); + break; + case NodeKind.Try: + clearStatements(statement.bodyStatements); + clearStatements(statement.catchStatements); + clearStatements(statement.finallyStatements); + break; + case NodeKind.TypeDeclaration: + clearTypeDeclaration(statement); + break; + case NodeKind.Variable: + for (const declaration of statement.declarations) { + clearVariableLike(declaration); + } + break; + case NodeKind.Void: + clearExpression(statement.expression); + break; + case NodeKind.While: + clearExpression(statement.condition); + clearStatement(statement.body); + break; + } +} + +function clearDeclaration(declaration) { + if (!declaration) return; + switch (declaration.kind) { + case NodeKind.ClassDeclaration: + case NodeKind.InterfaceDeclaration: + clearClassDeclaration(declaration); + break; + case NodeKind.EnumDeclaration: + for (const value of declaration.values) { + clearVariableLike(value); + } + break; + case NodeKind.FieldDeclaration: + case NodeKind.VariableDeclaration: + clearVariableLike(declaration); + break; + case NodeKind.FunctionDeclaration: + case NodeKind.MethodDeclaration: + clearFunctionDeclaration(declaration); + break; + case NodeKind.NamespaceDeclaration: + clearStatements(declaration.members); + break; + case NodeKind.TypeDeclaration: + clearTypeDeclaration(declaration); + break; + } +} + +function clearClassDeclaration(declaration) { + clearTypeParameters(declaration.typeParameters); + clearType(declaration.extendsType); + clearTypes(declaration.implementsTypes); + clearIndexSignature(declaration.indexSignature); + for (const member of declaration.members) { + clearDeclaration(member); + } +} + +function clearFunctionDeclaration(declaration) { + clearTypeParameters(declaration.typeParameters); + clearFunctionType(declaration.signature); + clearStatement(declaration.body); +} + +function clearTypeDeclaration(declaration) { + clearTypeParameters(declaration.typeParameters); + clearType(declaration.type); +} + +function clearVariableLike(declaration) { + clearType(declaration.type); + clearExpression(declaration.initializer); +} + +function clearExpression(expression) { + if (!expression) return; + switch (expression.kind) { + case NodeKind.Assertion: + clearExpression(expression.expression); + clearType(expression.toType); + break; + case NodeKind.Binary: + clearExpression(expression.left); + clearExpression(expression.right); + break; + case NodeKind.Call: + clearExpression(expression.expression); + clearTypes(expression.typeArguments); + clearExpressions(expression.args); + break; + case NodeKind.Class: + clearClassDeclaration(expression.declaration); + break; + case NodeKind.Comma: + clearExpressions(expression.expressions); + break; + case NodeKind.ElementAccess: + clearExpression(expression.expression); + clearExpression(expression.elementExpression); + break; + case NodeKind.Function: + clearFunctionDeclaration(expression.declaration); + break; + case NodeKind.InstanceOf: + clearExpression(expression.expression); + clearType(expression.isType); + break; + case NodeKind.Literal: + clearLiteral(expression); + break; + case NodeKind.New: + clearTypes(expression.typeArguments); + clearExpressions(expression.args); + break; + case NodeKind.Parenthesized: + clearExpression(expression.expression); + break; + case NodeKind.PropertyAccess: + clearExpression(expression.expression); + break; + case NodeKind.Ternary: + clearExpression(expression.condition); + clearExpression(expression.ifThen); + clearExpression(expression.ifElse); + break; + case NodeKind.UnaryPostfix: + case NodeKind.UnaryPrefix: + clearExpression(expression.operand); + break; + } +} + +function clearExpressions(expressions) { + if (!expressions) return; + for (const expression of expressions) { + clearExpression(expression); + } +} + +function clearLiteral(literal) { + switch (literal.literalKind) { + case LiteralKind.Array: + clearExpressions(literal.elementExpressions); + break; + case LiteralKind.Object: + clearExpressions(literal.values); + break; + case LiteralKind.Template: + clearExpressions(literal.expressions); + break; + } +} + +function clearType(type) { + if (!type) return; + switch (type.kind) { + case NodeKind.NamedType: + clearTypes(type.typeArguments); + break; + case NodeKind.FunctionType: + clearFunctionType(type); + break; + } +} + +function clearTypes(types) { + if (!types) return; + for (const type of types) { + clearType(type); + } +} + +function clearTypeParameters(typeParameters) { + if (!typeParameters) return; + for (const typeParameter of typeParameters) { + clearType(typeParameter.extendsType); + clearType(typeParameter.defaultType); + } +} + +function clearIndexSignature(indexSignature) { + if (!indexSignature) return; + clearType(indexSignature.keyType); + clearType(indexSignature.valueType); +} + +function clearFunctionType(signature) { + if (!signature) return; + signature.explicitThisDecorators = null; + clearType(signature.explicitThisType); + for (const parameter of signature.parameters) { + parameter.decorators = null; + clearType(parameter.type); + clearExpression(parameter.initializer); + } + clearType(signature.returnType); +} diff --git a/tests/transform/parameter-decorators.ts b/tests/transform/parameter-decorators.ts new file mode 100644 index 0000000000..c08652cfb4 --- /dev/null +++ b/tests/transform/parameter-decorators.ts @@ -0,0 +1,11 @@ +function regular(@first value: i32): void {} +function withthis(@self this: i32, @rest ...values: i32[]): i32 { return this; } + +class Box { + constructor(@field public value: i32) {} + method(@arg value: i32): void {} +} + +type Callback = (@arg value: i32) => void; +const expression = function(@arg value: i32): void {}; +const arrow = (@arg value: i32): void => {}; diff --git a/tests/transform/remove-parameter-decorators.js b/tests/transform/remove-parameter-decorators.js new file mode 100644 index 0000000000..1a24609361 --- /dev/null +++ b/tests/transform/remove-parameter-decorators.js @@ -0,0 +1,327 @@ +console.log("Parameter decorator removal transform loaded"); + +const NodeKind = { + NamedType: 1, + FunctionType: 2, + Assertion: 7, + Binary: 8, + Call: 9, + Class: 10, + Comma: 11, + ElementAccess: 12, + Function: 14, + InstanceOf: 15, + Literal: 16, + New: 17, + Parenthesized: 20, + PropertyAccess: 21, + Ternary: 22, + UnaryPostfix: 27, + UnaryPrefix: 28, + Block: 30, + Do: 33, + ExportDefault: 36, + Expression: 38, + For: 39, + ForOf: 40, + If: 41, + Return: 43, + Switch: 44, + Throw: 45, + Try: 46, + Variable: 47, + Void: 48, + While: 49, + ClassDeclaration: 51, + EnumDeclaration: 52, + FieldDeclaration: 54, + FunctionDeclaration: 55, + InterfaceDeclaration: 57, + MethodDeclaration: 58, + NamespaceDeclaration: 59, + TypeDeclaration: 60, + VariableDeclaration: 61 +}; + +const LiteralKind = { + Template: 3, + Array: 5, + Object: 6 +}; + +export function afterInitialize(program) { + console.log("- afterInitialize strip parameter decorators"); + for (const source of program.sources) { + clearStatements(source.statements); + } +} + +function clearStatements(statements) { + if (!statements) return; + for (const statement of statements) { + clearStatement(statement); + } +} + +function clearStatement(statement) { + if (!statement) return; + switch (statement.kind) { + case NodeKind.Block: + clearStatements(statement.statements); + break; + case NodeKind.ClassDeclaration: + case NodeKind.InterfaceDeclaration: + clearClassDeclaration(statement); + break; + case NodeKind.Do: + clearStatement(statement.body); + clearExpression(statement.condition); + break; + case NodeKind.EnumDeclaration: + for (const value of statement.values) { + clearVariableLike(value); + } + break; + case NodeKind.ExportDefault: + clearDeclaration(statement.declaration); + break; + case NodeKind.Expression: + clearExpression(statement.expression); + break; + case NodeKind.For: + clearStatement(statement.initializer); + clearExpression(statement.condition); + clearExpression(statement.incrementor); + clearStatement(statement.body); + break; + case NodeKind.ForOf: + clearStatement(statement.variable); + clearExpression(statement.iterable); + clearStatement(statement.body); + break; + case NodeKind.FunctionDeclaration: + case NodeKind.MethodDeclaration: + clearFunctionDeclaration(statement); + break; + case NodeKind.If: + clearExpression(statement.condition); + clearStatement(statement.ifTrue); + clearStatement(statement.ifFalse); + break; + case NodeKind.NamespaceDeclaration: + clearStatements(statement.members); + break; + case NodeKind.Return: + clearExpression(statement.value); + break; + case NodeKind.Switch: + clearExpression(statement.condition); + for (const switchCase of statement.cases) { + clearExpression(switchCase.label); + clearStatements(switchCase.statements); + } + break; + case NodeKind.Throw: + clearExpression(statement.value); + break; + case NodeKind.Try: + clearStatements(statement.bodyStatements); + clearStatements(statement.catchStatements); + clearStatements(statement.finallyStatements); + break; + case NodeKind.TypeDeclaration: + clearTypeDeclaration(statement); + break; + case NodeKind.Variable: + for (const declaration of statement.declarations) { + clearVariableLike(declaration); + } + break; + case NodeKind.Void: + clearExpression(statement.expression); + break; + case NodeKind.While: + clearExpression(statement.condition); + clearStatement(statement.body); + break; + } +} + +function clearDeclaration(declaration) { + if (!declaration) return; + switch (declaration.kind) { + case NodeKind.ClassDeclaration: + case NodeKind.InterfaceDeclaration: + clearClassDeclaration(declaration); + break; + case NodeKind.EnumDeclaration: + for (const value of declaration.values) { + clearVariableLike(value); + } + break; + case NodeKind.FieldDeclaration: + case NodeKind.VariableDeclaration: + clearVariableLike(declaration); + break; + case NodeKind.FunctionDeclaration: + case NodeKind.MethodDeclaration: + clearFunctionDeclaration(declaration); + break; + case NodeKind.NamespaceDeclaration: + clearStatements(declaration.members); + break; + case NodeKind.TypeDeclaration: + clearTypeDeclaration(declaration); + break; + } +} + +function clearClassDeclaration(declaration) { + clearTypeParameters(declaration.typeParameters); + clearType(declaration.extendsType); + clearTypes(declaration.implementsTypes); + clearIndexSignature(declaration.indexSignature); + for (const member of declaration.members) { + clearDeclaration(member); + } +} + +function clearFunctionDeclaration(declaration) { + clearTypeParameters(declaration.typeParameters); + clearFunctionType(declaration.signature); + clearStatement(declaration.body); +} + +function clearTypeDeclaration(declaration) { + clearTypeParameters(declaration.typeParameters); + clearType(declaration.type); +} + +function clearVariableLike(declaration) { + clearType(declaration.type); + clearExpression(declaration.initializer); +} + +function clearExpression(expression) { + if (!expression) return; + switch (expression.kind) { + case NodeKind.Assertion: + clearExpression(expression.expression); + clearType(expression.toType); + break; + case NodeKind.Binary: + clearExpression(expression.left); + clearExpression(expression.right); + break; + case NodeKind.Call: + clearExpression(expression.expression); + clearTypes(expression.typeArguments); + clearExpressions(expression.args); + break; + case NodeKind.Class: + clearClassDeclaration(expression.declaration); + break; + case NodeKind.Comma: + clearExpressions(expression.expressions); + break; + case NodeKind.ElementAccess: + clearExpression(expression.expression); + clearExpression(expression.elementExpression); + break; + case NodeKind.Function: + clearFunctionDeclaration(expression.declaration); + break; + case NodeKind.InstanceOf: + clearExpression(expression.expression); + clearType(expression.isType); + break; + case NodeKind.Literal: + clearLiteral(expression); + break; + case NodeKind.New: + clearTypes(expression.typeArguments); + clearExpressions(expression.args); + break; + case NodeKind.Parenthesized: + clearExpression(expression.expression); + break; + case NodeKind.PropertyAccess: + clearExpression(expression.expression); + break; + case NodeKind.Ternary: + clearExpression(expression.condition); + clearExpression(expression.ifThen); + clearExpression(expression.ifElse); + break; + case NodeKind.UnaryPostfix: + case NodeKind.UnaryPrefix: + clearExpression(expression.operand); + break; + } +} + +function clearExpressions(expressions) { + if (!expressions) return; + for (const expression of expressions) { + clearExpression(expression); + } +} + +function clearLiteral(literal) { + switch (literal.literalKind) { + case LiteralKind.Array: + clearExpressions(literal.elementExpressions); + break; + case LiteralKind.Object: + clearExpressions(literal.values); + break; + case LiteralKind.Template: + clearExpressions(literal.expressions); + break; + } +} + +function clearType(type) { + if (!type) return; + switch (type.kind) { + case NodeKind.NamedType: + clearTypes(type.typeArguments); + break; + case NodeKind.FunctionType: + clearFunctionType(type); + break; + } +} + +function clearTypes(types) { + if (!types) return; + for (const type of types) { + clearType(type); + } +} + +function clearTypeParameters(typeParameters) { + if (!typeParameters) return; + for (const typeParameter of typeParameters) { + clearType(typeParameter.extendsType); + clearType(typeParameter.defaultType); + } +} + +function clearIndexSignature(indexSignature) { + if (!indexSignature) return; + clearType(indexSignature.keyType); + clearType(indexSignature.valueType); +} + +function clearFunctionType(signature) { + if (!signature) return; + signature.explicitThisDecorators = null; + clearType(signature.explicitThisType); + for (const parameter of signature.parameters) { + parameter.decorators = null; + clearType(parameter.type); + clearExpression(parameter.initializer); + } + clearType(signature.returnType); +} From 932cb9ef547b8417dd2004346b3420b1f22f3f53 Mon Sep 17 00:00:00 2001 From: jtenner Date: Fri, 27 Mar 2026 00:05:01 -0400 Subject: [PATCH 4/6] Clarify transform-only parameter decorator contract --- src/ast.ts | 4 ++-- src/compiler.ts | 1 + src/parser.ts | 15 ++++++++++----- src/program.ts | 4 ++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index b01353ad8e..ba158f2f6b 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -930,7 +930,7 @@ export class FunctionTypeNode extends TypeNode { super(NodeKind.FunctionType, isNullable, range); } - /** Decorators on an explicit `this` parameter, if any. */ + /** Decorators on an explicit `this` parameter, if any, preserved for transforms. */ explicitThisDecorators: DecoratorNode[] | null = null; } @@ -977,7 +977,7 @@ export class ParameterNode extends Node { super(NodeKind.Parameter, range); } - /** Decorators, if any. */ + /** Decorators, if any, preserved so transforms can rewrite them before validation. */ decorators: DecoratorNode[] | null = null; /** Implicit field declaration, if applicable. */ implicitFieldDeclaration: FieldDeclaration | null = null; diff --git a/src/compiler.ts b/src/compiler.ts index 8785655609..12d04785c0 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -536,6 +536,7 @@ export class Compiler extends DiagnosticEmitter { // initialize lookup maps, built-ins, imports, exports, etc. this.program.initialize(); + // Reject any parameter decorators that transforms left on the AST. this.program.validateParameterDecorators(); diff --git a/src/parser.ts b/src/parser.ts index 30e0da9270..a5a14adec7 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -698,12 +698,13 @@ export class Parser extends DiagnosticEmitter { // Indicates whether tryParseSignature determined that it is handling a Signature private tryParseSignatureIsSignature: bool = false; - /** Parses a function type, as used in type declarations. */ + /** Parses a function type, preserving parameter decorators for transforms. */ tryParseFunctionType( tn: Tokenizer ): FunctionTypeNode | null { - // at '(': ('...'? Identifier '?'? ':' Type (',' '...'? Identifier '?'? ':' Type)* )? ')' '=>' Type + // at '(': (Decorator* ('...'? Identifier '?'? ':' Type | this ':' Type) + // (',' Decorator* ('...'? Identifier '?'? ':' Type | this ':' Type))* )? ')' '=>' Type let state = tn.mark(); let startPos = tn.tokenPos; @@ -937,6 +938,7 @@ export class Parser extends DiagnosticEmitter { private parseParameterDecorators( tn: Tokenizer ): DecoratorNode[] | null { + // Preserve parameter decorators in the AST so transforms can inspect or remove them later. let decorators: DecoratorNode[] | null = null; while (tn.skip(Token.At)) { let decorator = this.parseDecorator(tn); @@ -1249,7 +1251,9 @@ export class Parser extends DiagnosticEmitter { return null; } + /** Explicit `this` parameter captured by the current parseParameters call, if any. */ private parseParametersThis: NamedTypeNode | null = null; + /** Decorators on the explicit `this` parameter, preserved for transforms. */ private parseParametersThisDecorators: DecoratorNode[] | null = null; parseParameters( @@ -1257,7 +1261,7 @@ export class Parser extends DiagnosticEmitter { isConstructor: bool = false ): ParameterNode[] | null { - // at '(': (Parameter (',' Parameter)*)? ')' + // at '(': (Decorator* Parameter (',' Decorator* Parameter)*)? ')' let parameters = new Array(); let seenRest: ParameterNode | null = null; @@ -1265,7 +1269,7 @@ export class Parser extends DiagnosticEmitter { let reportedRest = false; let thisType: TypeNode | null = null; - // check if there is a leading `this` parameter + // check if there is a leading `this` parameter, preserving any decorators on it this.parseParametersThis = null; this.parseParametersThisDecorators = null; @@ -1361,7 +1365,8 @@ export class Parser extends DiagnosticEmitter { decorators: DecoratorNode[] | null = null ): ParameterNode | null { - // before: ('public' | 'private' | 'protected' | '...')? Identifier '?'? (':' Type)? ('=' Expression)? + // before: Decorator* ('public' | 'private' | 'protected' | 'readonly')? '...'? Identifier + // '?'? (':' Type)? ('=' Expression)? let isRest = false; let isOptional = false; diff --git a/src/program.ts b/src/program.ts index 2a8b6500f7..89edbb5a92 100644 --- a/src/program.ts +++ b/src/program.ts @@ -495,7 +495,7 @@ export class Program extends DiagnosticEmitter { nextSignatureId: i32 = 0; /** An indicator if the program has been initialized. */ initialized: bool = false; - /** An indicator if parameter decorators have been validated. */ + /** Indicates whether the one-shot post-transform parameter decorator validation has run. */ parameterDecoratorsValidated: bool = false; // Lookup maps @@ -1532,7 +1532,7 @@ export class Program extends DiagnosticEmitter { } } - /** Validates that no parameter decorators survive past transform time. */ + /** Rejects parameter decorators that survive transform time. These remain transform-only syntax. */ validateParameterDecorators(): void { if (this.parameterDecoratorsValidated) return; this.parameterDecoratorsValidated = true; From 64548d1ade646bf623ce45f19f805888e6e2e8be Mon Sep 17 00:00:00 2001 From: jtenner Date: Fri, 27 Mar 2026 00:09:11 -0400 Subject: [PATCH 5/6] Document transform hook timing for parameter decorators --- cli/index.d.ts | 5 ++++- cli/index.js | 2 +- src/index-wasm.ts | 2 +- tests/transform/cjs/remove-parameter-decorators.js | 2 ++ tests/transform/remove-parameter-decorators.js | 2 ++ 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cli/index.d.ts b/cli/index.d.ts index 58c7fb4ef6..3238d925b9 100644 --- a/cli/index.d.ts +++ b/cli/index.d.ts @@ -276,7 +276,10 @@ export abstract class Transform { /** Called when parsing is complete, before a program is instantiated from the AST. */ afterParse?(parser: Parser): void | Promise; - /** Called after the program is instantiated. */ + /** + * Called after the program is instantiated and before compilation-time validation runs. + * This is the last hook where transforms can rewrite preserved AST-only syntax before it is rejected. + */ afterInitialize?(program: Program): void | Promise; /** Called when compilation is complete, before the module is being validated. */ diff --git a/cli/index.js b/cli/index.js index d6808957f4..4d8939cb4d 100644 --- a/cli/index.js +++ b/cli/index.js @@ -718,7 +718,7 @@ export async function main(argv, options) { stats.initializeTime += stats.end(begin); } - // Call afterInitialize transform hook + // Call afterInitialize transform hook, the last AST rewrite point before compilation-time validation. { let error = await applyTransform("afterInitialize", program); if (error) return prepareResult(error); diff --git a/src/index-wasm.ts b/src/index-wasm.ts index ec51de73da..15a5ff4347 100644 --- a/src/index-wasm.ts +++ b/src/index-wasm.ts @@ -347,7 +347,7 @@ export function getDependee(program: Program, file: string): string | null { // Compiler -/** Initializes the program pre-emptively for transform hooks. */ +/** Initializes the program pre-emptively so `afterInitialize` transforms can rewrite the AST before compilation. */ export function initializeProgram(program: Program): void { program.initialize(); } diff --git a/tests/transform/cjs/remove-parameter-decorators.js b/tests/transform/cjs/remove-parameter-decorators.js index 80168e221c..2eee0745c3 100644 --- a/tests/transform/cjs/remove-parameter-decorators.js +++ b/tests/transform/cjs/remove-parameter-decorators.js @@ -1,3 +1,5 @@ +// Example transform proving that preserved parameter decorators can be stripped +// during afterInitialize before compilation rejects them. console.log("CommonJS parameter decorator removal transform loaded"); const NodeKind = { diff --git a/tests/transform/remove-parameter-decorators.js b/tests/transform/remove-parameter-decorators.js index 1a24609361..046cc5b67e 100644 --- a/tests/transform/remove-parameter-decorators.js +++ b/tests/transform/remove-parameter-decorators.js @@ -1,3 +1,5 @@ +// Example transform proving that preserved parameter decorators can be stripped +// during afterInitialize before compilation rejects them. console.log("Parameter decorator removal transform loaded"); const NodeKind = { From 4ec33710c33d957ce2a4f1b86d22060a28978a15 Mon Sep 17 00:00:00 2001 From: jtenner Date: Fri, 27 Mar 2026 00:27:53 -0400 Subject: [PATCH 6/6] Fix lint for parameter decorator fixtures --- .eslintrc.cjs | 6 ++++++ src/program.ts | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 3ba62e3992..6ecf0a15f4 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -2,6 +2,12 @@ module.exports = { root: true, + ignorePatterns: [ + // These fixtures intentionally exercise AssemblyScript-only syntax that + // TypeScript's parser rejects before lint rules can run. + "tests/compiler/parameter-decorators.ts", + "tests/transform/parameter-decorators.ts" + ], parser: "@typescript-eslint/parser", plugins: [ "@typescript-eslint", diff --git a/src/program.ts b/src/program.ts index 89edbb5a92..c44887bca1 100644 --- a/src/program.ts +++ b/src/program.ts @@ -129,7 +129,6 @@ import { IndexSignatureNode, InterfaceDeclaration, MethodDeclaration, - ModuleDeclaration, NamespaceDeclaration, ReturnStatement, SwitchCase,