From 2173a84bc4d995bf28d239f335feaba59eff978c Mon Sep 17 00:00:00 2001 From: jtenner Date: Thu, 26 Mar 2026 23:49:00 -0400 Subject: [PATCH 1/7] 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 9cf07692ef33561c061908c5b7d82317d7b80f02 Mon Sep 17 00:00:00 2001 From: jtenner Date: Thu, 26 Mar 2026 23:53:14 -0400 Subject: [PATCH 2/7] 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 7e4c3c9c40..bf83fb24b1 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 e54826b19d..b3bcac543e 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 cac365b283ee9906fb12f4fcfdbeda85fd8b1deb Mon Sep 17 00:00:00 2001 From: jtenner Date: Thu, 26 Mar 2026 23:57:59 -0400 Subject: [PATCH 3/7] 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 f1ed7f85d1..3602d259ab 100644 --- a/package.json +++ b/package.json @@ -85,8 +85,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 11038562d72f0d8314bf2ce2a5015e281fdc03bf Mon Sep 17 00:00:00 2001 From: jtenner Date: Fri, 27 Mar 2026 00:05:01 -0400 Subject: [PATCH 4/7] 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 bf83fb24b1..2c122846b2 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 b3bcac543e..56b98211ad 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 0d59a953cd9524817d00ef99d6936c7d83d1bfad Mon Sep 17 00:00:00 2001 From: jtenner Date: Fri, 27 Mar 2026 00:09:11 -0400 Subject: [PATCH 5/7] 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 7e202e4f61..50866d3b5f 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 4786023bd321f63bbdd6db62c85a70c2c56fa6bd Mon Sep 17 00:00:00 2001 From: jtenner Date: Fri, 27 Mar 2026 00:27:53 -0400 Subject: [PATCH 6/7] Fix lint for parameter decorator fixtures --- .eslintrc.cjs | 274 +++++++++++++++++++++++++++++++++++++++++++++++++ src/program.ts | 1 - 2 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 .eslintrc.cjs diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000000..6ecf0a15f4 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,274 @@ +/* global module */ + +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", + ], + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + ], + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + ecmaFeatures: {} + }, + globals: { + "globalThis": "readonly", + "BigInt64Array": "readonly", + "BigUint64Array": "readonly", + "WebAssembly": "readonly", + "FinalizationRegistry": "readonly", + "fetch": "readonly", + "URL": "readonly", + "console": "readonly" + }, + + // === General rules ========================================================= + + rules: { + // Omitted semicolons are hugely popular, yet within the compiler it makes + // sense to be better safe than sorry. + "semi": "error", + + // Our code bases uses 2 spaces for indentation, and we enforce it here so + // files don't mix spaces, tabs or different indentation levels. + "indent": ["error", 2, { + "SwitchCase": 1, + "VariableDeclarator": "first", + "offsetTernaryExpressions": true, + "ignoredNodes": [ // FIXME: something's odd here + "ConditionalExpression > *", + "ConditionalExpression > * > *", + "ConditionalExpression > * > * > *" + ] + }], + + // This is mostly visual style, making comments look uniform. + "spaced-comment": ["error", "always", { + "markers": ["/"], // triple-slash + "exceptions": ["/"] // all slashes + }], + + // This tends to be annoying as it encourages developers to make everything + // that is never reassigned a 'const', sometimes semantically incorrect so, + // typically leading to huge diffs in follow-up PRs modifying affected code. + "prefer-const": "off", + + // It is perfectly fine to declare top-level variables with `var`, yet this + // rule doesn't provide configuration options that would help. + "no-var": "off", + + // Quite often, dealing with multiple related cases at once or otherwise + // falling through is exactly the point of using a switch. + "no-fallthrough": "off", + + // Typical false-positives here are `do { ... } while (true)` statements or + // similar, but the only option provided here is not checking any loops. + "no-constant-condition": ["error", { checkLoops: false }], + + // Functions are nested in blocks occasionally, and there haven't been any + // problems with this so far, so turning the check off. + "no-inner-declarations": "off", + + // Quite common in scenarios where an iteration starts at `current = this`. + "@typescript-eslint/no-this-alias": "off", + + // Interferes with tests and 64-bit literals + "@typescript-eslint/no-loss-of-precision": "off", + + // Disabled here, but enabled again for JavaScript files. + "no-unused-vars": "off", + + // Disabled here, but enabled again for TypeScript files. + "@typescript-eslint/no-unused-vars": "off" + }, + overrides: [ + + // === JavaScript rules ==================================================== + + { + env: { + "browser": true, + "amd": true, + "node": true, + "es6": true + }, + files: [ + "**/*.js", + "bin/*" + ], + rules: { + // We are testing both ESM and UMD, so don't limit us. + "@typescript-eslint/no-var-requires": "off", + + // This rule does not behave well in JS files. + "@typescript-eslint/explicit-module-boundary-types": "off", + + // Enforcing to remove function parameters on stubs makes code less + // maintainable, so we instead allow unused function parameters. + "no-unused-vars": [ + "warn", { + "vars": "local", + "args": "none", + "ignoreRestSiblings": false + } + ], + + "@typescript-eslint/no-loss-of-precision": "error", + } + }, + + // === TypeScript rules ==================================================== + + { + files: [ + "**/*.ts" + ], + rules: { + // Enforcing to remove function parameters on stubs makes code less + // maintainable, so we instead allow unused function parameters. + "@typescript-eslint/no-unused-vars": [ + "warn", { + "vars": "local", + "varsIgnorePattern": "^[A-Z](?:From|To)?$", // ignore type params + "args": "none", + "ignoreRestSiblings": false + } + ] + } + }, + + // === AssemblyScript rules (extends TypeScript rules) ===================== + + { + files: [ + "**/assembly/**/*.ts", + "src/**/*.ts", + "lib/parse/src/**/*.ts" + ], + rules: { + // Namespaces are quite useful in AssemblyScript + "@typescript-eslint/no-namespace": "off", + + // There is actually codegen difference here + "@typescript-eslint/no-array-constructor": "off", + + // Sometimes it can't be avoided to add a @ts-ignore + "@typescript-eslint/ban-ts-comment": "off", + + // Utilized to achieve portability in some cases + "@typescript-eslint/no-non-null-assertion": "off", + } + }, + + // === Compiler rules (extends AssemblyScript rules) ======================= + + { + files: [ + "src/**/*.ts", + "std/assembly/**/*.ts" + ], + rules: { + // There is an actual codegen difference here - TODO: revisit + "no-cond-assign": "off", + + // Not all types can be omitted in AS yet - TODO: revisit + "@typescript-eslint/no-inferrable-types": "off", + + // Used rarely to reference internals that are not user-visible + "@typescript-eslint/triple-slash-reference": "off", + + // The compiler has its own `Function` class for example + "no-shadow-restricted-names": "off", + "@typescript-eslint/ban-types": "off" + } + }, + + // === Standard Library rules (extends AssemblyScript rules) =============== + + { + files: [ + "std/assembly/**/*.ts" + ], + rules: { + // We are implementing with --noLib, so we shadow all the time + "no-shadow-restricted-names": "off", + + // Similarly, sometimes we need the return type to be String, not string + "@typescript-eslint/ban-types": "off" + } + }, + + // === Standard Definition rules (extends TypeScript rules) ================ + + { + files: [ + "std/**/*.d.ts" + ], + rules: { + // Often required to achieve compatibility with TypeScript + "@typescript-eslint/no-explicit-any": "off", + + // Interfaces can be stubs here, i.e. not yet fully implemented + "@typescript-eslint/no-empty-interface": "off", + + // Definitions make use of `object` to model rather unusual constraints + "@typescript-eslint/ban-types": "off" + } + }, + + // === Compiler Definition rules (extends TypeScript rules) ================ + + { + files: [ + "./dist/*.d.ts" + ], + rules: { + // Our definitions are complicated, and all attempts to describe them + // as modules have failed so far. As such, we re-export namespaces. + "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/triple-slash-reference": "off" + } + }, + + // === Test rules (extends TypeScript rules) =============================== + + { + files: [ + "./tests/compiler/**/*.ts", + "./lib/loader/tests/assembly/**/*.ts" + ], + rules: { + // Tests typically include unusual code patterns on purpose. This is + // very likely not an extensive list, but covers what's there so far. + "no-empty": "off", + "no-cond-assign": "off", + "no-compare-neg-zero": "off", + "no-inner-declarations": "off", + "no-constant-condition": "off", + "use-isnan": "off", + "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-extra-semi": "off", + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/triple-slash-reference": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-extra-non-null-assertion": "off", + "@typescript-eslint/no-empty-interface": "off" + } + }, + ] +}; diff --git a/src/program.ts b/src/program.ts index 56b98211ad..f2c7cb5184 100644 --- a/src/program.ts +++ b/src/program.ts @@ -129,7 +129,6 @@ import { IndexSignatureNode, InterfaceDeclaration, MethodDeclaration, - ModuleDeclaration, NamespaceDeclaration, ReturnStatement, SwitchCase, From bd029c6a1f8492a3d454fe20921105cc4242c939 Mon Sep 17 00:00:00 2001 From: jtenner Date: Sun, 5 Apr 2026 17:10:42 -0400 Subject: [PATCH 7/7] chore: ignore AS parameter decorator fixture in eslint --- eslint.config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eslint.config.js b/eslint.config.js index 4c53f86f2b..7b6049f12e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -25,6 +25,10 @@ export default defineConfig([ // FIXME: Tagged template literal tests with invalid escapes "tests/compiler/templateliteral.ts", + + // Decorators on `this` are not allowed typically in TypeScript, but this + // fixture exercises that AS-only syntax and is validated by transform tests. + "tests/transform/parameter-decorators.ts", ]), js.configs.recommended,