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/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, 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/src/ast.ts b/src/ast.ts index 01d8e9a421..f31da34c8c 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -155,10 +155,11 @@ export abstract class Node { parameters: ParameterNode[], returnType: TypeNode, explicitThisType: NamedTypeNode | null, + explicitThisDecorators: DecoratorNode[] | null, isNullable: bool, range: Range ): FunctionTypeNode { - return new FunctionTypeNode(parameters, returnType, explicitThisType, isNullable, range); + return new FunctionTypeNode(parameters, returnType, explicitThisType, explicitThisDecorators, isNullable, range); } static createOmittedType( @@ -181,9 +182,10 @@ export abstract class Node { name: IdentifierExpression, type: TypeNode, initializer: Expression | null, + decorators: DecoratorNode[] | null, range: Range ): ParameterNode { - return new ParameterNode(parameterKind, name, type, initializer, range); + return new ParameterNode(parameterKind, name, type, initializer, decorators, range); } // special @@ -919,6 +921,8 @@ export class FunctionTypeNode extends TypeNode { public returnType: TypeNode, /** Explicitly provided this type, if any. */ public explicitThisType: NamedTypeNode | null, // can't be a function + /** Decorators on an explicit `this` parameter, if any, preserved for transforms. */ + public explicitThisDecorators: DecoratorNode[] | null, /** Whether nullable or not. */ isNullable: bool, /** Source range. */ @@ -965,12 +969,13 @@ export class ParameterNode extends Node { public type: TypeNode, /** Initializer expression, if any. */ public initializer: Expression | null, + /** Decorators, if any, preserved so transforms can rewrite them before validation. */ + public decorators: DecoratorNode[] | null, /** Source range. */ range: Range ) { super(NodeKind.Parameter, range); } - /** Implicit field declaration, if applicable. */ implicitFieldDeclaration: FieldDeclaration | null = null; /** Common flags indicating specific traits. */ @@ -1664,6 +1669,8 @@ export class Source extends Node { debugInfoIndex: i32 = -1; /** Re-exported sources. */ exportPaths: string[] | null = null; + /** Source-level statements that preserved parameter decorators while parsing. */ + parameterDecoratorStatements: Statement[] | null = null; /** Checks if this source represents native code. */ get isNative(): bool { @@ -2399,6 +2406,383 @@ export function findDecorator(kind: DecoratorKind, decorators: DecoratorNode[] | return null; } +/** Generic AST walker. */ +export abstract class NodeWalker { + + /** Indicates whether walking has been stopped. */ + stopped: bool = false; + + /** Visits a node and its children unless walking has been stopped. */ + visitNode(node: Node | null): void { + if (!node || this.stopped) return; + if (!this.visit(node) || this.stopped) return; + switch (node.kind) { + case NodeKind.Source: { + this.visitNodes((node).statements); + break; + } + case NodeKind.TypeName: { + let current: TypeName | null = node; + while (current) { + this.visitNode(current.identifier); + current = current.next; + } + break; + } + case NodeKind.NamedType: { + let namedType = node; + this.visitNode(namedType.name); + this.visitNodes(namedType.typeArguments); + break; + } + case NodeKind.FunctionType: { + let functionType = node; + this.visitNodes(functionType.explicitThisDecorators); + this.visitNode(functionType.explicitThisType); + this.visitNodes(functionType.parameters); + this.visitNode(functionType.returnType); + break; + } + case NodeKind.TypeParameter: { + let typeParameter = node; + this.visitNode(typeParameter.name); + this.visitNode(typeParameter.extendsType); + this.visitNode(typeParameter.defaultType); + break; + } + + case NodeKind.False: + case NodeKind.Null: + case NodeKind.Super: + case NodeKind.This: + case NodeKind.True: + case NodeKind.Constructor: + case NodeKind.Identifier: + case NodeKind.Omitted: { + break; + } + case NodeKind.Assertion: { + let assertion = node; + this.visitNode(assertion.expression); + this.visitNode(assertion.toType); + break; + } + case NodeKind.Binary: { + let binary = node; + this.visitNode(binary.left); + this.visitNode(binary.right); + break; + } + case NodeKind.Call: { + let call = node; + this.visitNode(call.expression); + this.visitNodes(call.typeArguments); + this.visitNodes(call.args); + break; + } + case NodeKind.Class: { + this.visitNode((node).declaration); + break; + } + case NodeKind.Comma: { + this.visitNodes((node).expressions); + break; + } + case NodeKind.ElementAccess: { + let elementAccess = node; + this.visitNode(elementAccess.expression); + this.visitNode(elementAccess.elementExpression); + break; + } + case NodeKind.Function: { + this.visitNode((node).declaration); + break; + } + case NodeKind.InstanceOf: { + let instanceOf = node; + this.visitNode(instanceOf.expression); + this.visitNode(instanceOf.isType); + break; + } + case NodeKind.Literal: { + let literal = node; + switch (literal.literalKind) { + case LiteralKind.Array: { + this.visitNodes((literal).elementExpressions); + break; + } + case LiteralKind.Object: { + let objectLiteral = literal; + this.visitNodes(objectLiteral.names); + this.visitNodes(objectLiteral.values); + break; + } + case LiteralKind.Template: { + let templateLiteral = literal; + this.visitNode(templateLiteral.tag); + this.visitNodes(templateLiteral.expressions); + break; + } + } + break; + } + case NodeKind.New: { + let newExpression = node; + this.visitNode(newExpression.typeName); + this.visitNodes(newExpression.typeArguments); + this.visitNodes(newExpression.args); + break; + } + case NodeKind.Parenthesized: { + this.visitNode((node).expression); + break; + } + case NodeKind.PropertyAccess: { + let propertyAccess = node; + this.visitNode(propertyAccess.expression); + this.visitNode(propertyAccess.property); + break; + } + case NodeKind.Ternary: { + let ternary = node; + this.visitNode(ternary.condition); + this.visitNode(ternary.ifThen); + this.visitNode(ternary.ifElse); + break; + } + case NodeKind.UnaryPostfix: { + this.visitNode((node).operand); + break; + } + case NodeKind.UnaryPrefix: { + this.visitNode((node).operand); + break; + } + + case NodeKind.Block: { + this.visitNodes((node).statements); + break; + } + case NodeKind.Break: { + this.visitNode((node).label); + break; + } + case NodeKind.Continue: { + this.visitNode((node).label); + break; + } + case NodeKind.Do: { + let doStatement = node; + this.visitNode(doStatement.body); + this.visitNode(doStatement.condition); + break; + } + case NodeKind.Empty: + case NodeKind.Module: { + break; + } + case NodeKind.ExportImport: { + let exportImport = node; + this.visitNode(exportImport.name); + this.visitNode(exportImport.externalName); + break; + } + case NodeKind.Export: { + let exportStatement = node; + this.visitNodes(exportStatement.members); + this.visitNode(exportStatement.path); + break; + } + case NodeKind.ExportDefault: { + this.visitNode((node).declaration); + break; + } + case NodeKind.Expression: { + this.visitNode((node).expression); + break; + } + case NodeKind.For: { + let forStatement = node; + this.visitNode(forStatement.initializer); + this.visitNode(forStatement.condition); + this.visitNode(forStatement.incrementor); + this.visitNode(forStatement.body); + break; + } + case NodeKind.ForOf: { + let forOfStatement = node; + this.visitNode(forOfStatement.variable); + this.visitNode(forOfStatement.iterable); + this.visitNode(forOfStatement.body); + break; + } + case NodeKind.If: { + let ifStatement = node; + this.visitNode(ifStatement.condition); + this.visitNode(ifStatement.ifTrue); + this.visitNode(ifStatement.ifFalse); + break; + } + case NodeKind.Import: { + let importStatement = node; + this.visitNodes(importStatement.declarations); + this.visitNode(importStatement.namespaceName); + this.visitNode(importStatement.path); + break; + } + case NodeKind.Return: { + this.visitNode((node).value); + break; + } + case NodeKind.Switch: { + let switchStatement = node; + this.visitNode(switchStatement.condition); + this.visitNodes(switchStatement.cases); + break; + } + case NodeKind.Throw: { + this.visitNode((node).value); + break; + } + case NodeKind.Try: { + let tryStatement = node; + this.visitNodes(tryStatement.bodyStatements); + this.visitNode(tryStatement.catchVariable); + this.visitNodes(tryStatement.catchStatements); + this.visitNodes(tryStatement.finallyStatements); + break; + } + case NodeKind.Variable: { + let variableStatement = node; + this.visitNodes(variableStatement.decorators); + this.visitNodes(variableStatement.declarations); + break; + } + case NodeKind.Void: { + this.visitNode((node).expression); + break; + } + case NodeKind.While: { + let whileStatement = node; + this.visitNode(whileStatement.condition); + this.visitNode(whileStatement.body); + break; + } + + case NodeKind.ClassDeclaration: + case NodeKind.InterfaceDeclaration: { + let classDeclaration = node; + this.visitDeclarationStatement(classDeclaration); + this.visitNodes(classDeclaration.typeParameters); + this.visitNode(classDeclaration.extendsType); + this.visitNodes(classDeclaration.implementsTypes); + this.visitNode(classDeclaration.indexSignature); + this.visitNodes(classDeclaration.members); + break; + } + case NodeKind.EnumDeclaration: { + let enumDeclaration = node; + this.visitDeclarationStatement(enumDeclaration); + this.visitNodes(enumDeclaration.values); + break; + } + case NodeKind.EnumValueDeclaration: + case NodeKind.FieldDeclaration: + case NodeKind.VariableDeclaration: { + this.visitVariableLikeDeclaration(node); + break; + } + case NodeKind.FunctionDeclaration: + case NodeKind.MethodDeclaration: { + let functionDeclaration = node; + this.visitDeclarationStatement(functionDeclaration); + this.visitNodes(functionDeclaration.typeParameters); + this.visitNode(functionDeclaration.signature); + this.visitNode(functionDeclaration.body); + break; + } + case NodeKind.ImportDeclaration: { + let importDeclaration = node; + this.visitDeclarationStatement(importDeclaration); + this.visitNode(importDeclaration.foreignName); + break; + } + case NodeKind.NamespaceDeclaration: { + let namespaceDeclaration = node; + this.visitDeclarationStatement(namespaceDeclaration); + this.visitNodes(namespaceDeclaration.members); + break; + } + case NodeKind.TypeDeclaration: { + let typeDeclaration = node; + this.visitDeclarationStatement(typeDeclaration); + this.visitNodes(typeDeclaration.typeParameters); + this.visitNode(typeDeclaration.type); + break; + } + + case NodeKind.Decorator: { + let decorator = node; + this.visitNode(decorator.name); + this.visitNodes(decorator.args); + break; + } + case NodeKind.ExportMember: { + let exportMember = node; + this.visitNode(exportMember.localName); + this.visitNode(exportMember.exportedName); + break; + } + case NodeKind.Parameter: { + let parameter = node; + this.visitNodes(parameter.decorators); + this.visitNode(parameter.name); + this.visitNode(parameter.type); + this.visitNode(parameter.initializer); + break; + } + case NodeKind.SwitchCase: { + let switchCase = node; + this.visitNode(switchCase.label); + this.visitNodes(switchCase.statements); + break; + } + case NodeKind.IndexSignature: { + let indexSignature = node; + this.visitNode(indexSignature.keyType); + this.visitNode(indexSignature.valueType); + break; + } + default: assert(false); + } + } + + /** Visits a declaration statement's common children. */ + protected visitDeclarationStatement(node: DeclarationStatement): void { + this.visitNode(node.name); + this.visitNodes(node.decorators); + } + + /** Visits a variable-like declaration statement's common children. */ + protected visitVariableLikeDeclaration(node: VariableLikeDeclarationStatement): void { + this.visitDeclarationStatement(node); + this.visitNode(node.type); + this.visitNode(node.initializer); + } + + /** Visits a possibly-null array of nodes. */ + protected visitNodes(nodes: T[] | null): void { + if (!nodes || this.stopped) return; + for (let i = 0, k = nodes.length; i < k; ++i) { + this.visitNode(nodes[i]); + if (this.stopped) break; + } + } + + /** Called for each visited node. Return `false` to skip its children. */ + abstract visit(node: Node): bool; +} + /** Mangles an external to an internal path. */ export function mangleInternalPath(path: string): string { if (path.endsWith("/")) { diff --git a/src/compiler.ts b/src/compiler.ts index 7e4c3c9c40..2c122846b2 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -536,6 +536,8 @@ 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(); // Binaryen treats all function references as being leaked to the outside world when diff --git a/src/extra/ast.ts b/src/extra/ast.ts index ebe9217f90..5ac1233683 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); } @@ -1541,7 +1543,7 @@ export class ASTBuilder { // other - serializeDecorator(node: DecoratorNode): void { + serializeDecorator(node: DecoratorNode, isInline: bool = false): void { let sb = this.sb; sb.push("@"); this.visitNode(node.name); @@ -1556,16 +1558,28 @@ export class ASTBuilder { this.visitNode(args[i]); } } - sb.push(")\n"); + sb.push(")"); + } + if (isInline) { + sb.push(" "); } else { sb.push("\n"); + indent(sb, this.indentLevel); + } + } + + serializeParameterDecorators(decorators: DecoratorNode[] | null): void { + if (decorators) { + for (let i = 0, k = decorators.length; i < k; ++i) { + this.serializeDecorator(decorators[i], true); + } } - indent(sb, this.indentLevel); } 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/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/src/parser.ts b/src/parser.ts index 7c69843973..53f289c5c6 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -118,6 +118,10 @@ export class Parser extends DiagnosticEmitter { sources: Source[]; /** Current overridden module name. */ currentModuleName: string | null = null; + /** Nesting depth of source-level statements while parsing. */ + currentSourceStatementDepth: i32 = 0; + /** Indicates whether the current source-level statement preserved any parameter decorators. */ + currentSourceStatementHasParameterDecorators: bool = false; /** Constructs a new parser. */ constructor( @@ -197,6 +201,10 @@ export class Parser extends DiagnosticEmitter { tn: Tokenizer, namespace: NamespaceDeclaration | null = null ): Statement | null { + let isSourceStatement = this.currentSourceStatementDepth++ == 0; + if (isSourceStatement) { + this.currentSourceStatementHasParameterDecorators = false; + } let flags = namespace ? namespace.flags & CommonFlags.Ambient : CommonFlags.None; let startPos = -1; @@ -429,7 +437,8 @@ export class Parser extends DiagnosticEmitter { case NodeKind.ClassDeclaration: case NodeKind.InterfaceDeclaration: case NodeKind.NamespaceDeclaration: { - return Node.createExportDefaultStatement(statement, tn.range(startPos, tn.pos)); + statement = Node.createExportDefaultStatement(statement, tn.range(startPos, tn.pos)); + break; } default: { this.error( @@ -439,6 +448,16 @@ export class Parser extends DiagnosticEmitter { } } } + --this.currentSourceStatementDepth; + if (isSourceStatement && statement != null && this.currentSourceStatementHasParameterDecorators) { + let source = assert(this.currentSource); + let parameterDecoratorStatements = source.parameterDecoratorStatements; + if (!parameterDecoratorStatements) { + source.parameterDecoratorStatements = [ statement ]; + } else { + parameterDecoratorStatements.push(statement); + } + } return statement; } @@ -698,17 +717,19 @@ 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; 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 +744,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; @@ -744,7 +771,9 @@ export class Parser extends DiagnosticEmitter { this.tryParseSignatureIsSignature = true; return null; } + this.tryParseParameterDecorators(tn); thisType = type; + thisDecorators = decorators; } else { tn.reset(state); this.tryParseSignatureIsSignature = false; @@ -773,10 +802,12 @@ export class Parser extends DiagnosticEmitter { this.tryParseSignatureIsSignature = isSignature; return null; } - let param = Node.createParameter(kind, name, type, null, tn.range(paramStart, tn.pos)); + this.tryParseParameterDecorators(tn); + let param = Node.createParameter(kind, name, type, null, decorators, tn.range(paramStart, tn.pos)); if (!parameters) parameters = [ param ]; else parameters.push(param); } else { + this.tryParseParameterDecorators(tn); if (!isSignature) { if (tn.peek() == Token.Comma) { isSignature = true; @@ -784,7 +815,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, decorators, tn.range(paramStart, tn.pos)); if (!parameters) parameters = [ param ]; else parameters.push(param); this.error( @@ -838,6 +869,7 @@ export class Parser extends DiagnosticEmitter { firstParamNameNoType, Node.createOmittedType(firstParamNameNoType.range.atEnd), null, + null, firstParamNameNoType.range ); if (!parameters) parameters = [ param ]; @@ -869,13 +901,16 @@ export class Parser extends DiagnosticEmitter { if (!parameters) parameters = []; - return Node.createFunctionType( + let functionType = Node.createFunctionType( parameters, returnType, thisType, + thisDecorators, false, tn.range(startPos, tn.pos) ); + this.noteFunctionTypeParameterDecorators(functionType); + return functionType; } // statements @@ -924,6 +959,46 @@ export class Parser extends DiagnosticEmitter { return null; } + 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); + if (!decorator) break; + if (!decorators) decorators = [decorator]; + else decorators.push(decorator); + } + return decorators; + } + + /** Tries to parse decorators that appear after a parameter has already started and reports them. */ + private tryParseParameterDecorators(tn: Tokenizer): void { + let decorators = this.parseParameterDecorators(tn); + if (decorators) { + this.error( + DiagnosticCode.Decorators_are_not_valid_here, + Range.join(decorators[0].range, decorators[decorators.length - 1].range) + ); + } + } + + /** Remembers when a source-level statement preserved parameter decorators in one of its function signatures. */ + private noteFunctionTypeParameterDecorators(signature: FunctionTypeNode): void { + if (signature.explicitThisDecorators) { + this.currentSourceStatementHasParameterDecorators = true; + return; + } + let parameters = signature.parameters; + for (let i = 0, k = parameters.length; i < k; ++i) { + if (parameters[i].decorators) { + this.currentSourceStatementHasParameterDecorators = true; + return; + } + } + } + parseVariable( tn: Tokenizer, flags: CommonFlags, @@ -1226,14 +1301,17 @@ 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( tn: Tokenizer, isConstructor: bool = false ): ParameterNode[] | null { - // at '(': (Parameter (',' Parameter)*)? ')' + // at '(': (Decorator* Parameter (',' Decorator* Parameter)*)? ')' let parameters = new Array(); let seenRest: ParameterNode | null = null; @@ -1241,42 +1319,53 @@ 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; - 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 + ); + } + this.tryParseParameterDecorators(tn); } 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 +1394,7 @@ export class Parser extends DiagnosticEmitter { } } parameters.push(param); + first = false; if (!tn.skip(Token.Comma)) { if (tn.skip(Token.CloseParen)) { break; @@ -1322,24 +1412,28 @@ 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)? + // before: Decorator* ('public' | 'private' | 'protected' | 'readonly')? '...'? 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 +1455,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)) { @@ -1377,12 +1471,14 @@ export class Parser extends DiagnosticEmitter { ); } } + this.tryParseParameterDecorators(tn); if (tn.skip(Token.Colon)) { type = this.parseType(tn); if (!type) return null; } else { type = Node.createOmittedType(tn.range(tn.pos)); } + this.tryParseParameterDecorators(tn); let initializer: Expression | null = null; if (tn.skip(Token.Equals)) { if (isRest) { @@ -1411,6 +1507,7 @@ export class Parser extends DiagnosticEmitter { identifier, type, initializer, + decorators, Range.join(assert(startRange), tn.range()) ); param.flags |= accessFlags; @@ -1520,9 +1617,11 @@ export class Parser extends DiagnosticEmitter { parameters, returnType, thisType, + this.parseParametersThisDecorators, false, tn.range(signatureStart, tn.pos) ); + this.noteFunctionTypeParameterDecorators(signature); let body: Statement | null = null; if (tn.skip(Token.OpenBrace)) { @@ -1597,7 +1696,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 +1713,7 @@ export class Parser extends DiagnosticEmitter { name: IdentifierExpression, parameters: ParameterNode[], explicitThis: NamedTypeNode | null, + explicitThisDecorators: DecoratorNode[] | null, arrowKind: ArrowKind, startPos: i32 = -1, signatureStart: i32 = -1 @@ -1634,9 +1743,11 @@ export class Parser extends DiagnosticEmitter { parameters, returnType, explicitThis, + explicitThisDecorators, false, tn.range(signatureStart, tn.pos) ); + this.noteFunctionTypeParameterDecorators(signature); let body: Statement | null = null; if (arrowKind) { @@ -2279,9 +2390,11 @@ export class Parser extends DiagnosticEmitter { parameters, returnType, thisType, + this.parseParametersThisDecorators, false, tn.range(signatureStart, tn.pos) ); + this.noteFunctionTypeParameterDecorators(signature); let body: Statement | null = null; if (tn.skip(Token.OpenBrace)) { @@ -3729,6 +3842,7 @@ export class Parser extends DiagnosticEmitter { Node.createEmptyIdentifierExpression(tn.range(startPos)), [], null, + null, ArrowKind.Parenthesized ); } @@ -3738,6 +3852,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); @@ -3926,10 +4041,12 @@ export class Parser extends DiagnosticEmitter { identifier, Node.createOmittedType(identifier.range.atEnd), null, + null, identifier.range ) ], null, + null, ArrowKind.Single, startPos ); diff --git a/src/program.ts b/src/program.ts index e5e27ebc59..e140a6b7a5 100644 --- a/src/program.ts +++ b/src/program.ts @@ -111,7 +111,8 @@ import { VariableStatement, ParameterKind, ParameterNode, - TypeName + TypeName, + NodeWalker } from "./ast"; import { @@ -247,6 +248,35 @@ export enum OperatorKind { // LogicalOr // a || b } +class ParameterDecoratorValidator extends NodeWalker { + constructor(private diagnostics: DiagnosticEmitter) { + super(); + } + + visit(node: Node): bool { + switch (node.kind) { + case NodeKind.FunctionType: { + this.reportParameterDecorators((node).explicitThisDecorators); + break; + } + case NodeKind.Parameter: { + this.reportParameterDecorators((node).decorators); + break; + } + } + return true; + } + + private reportParameterDecorators(decorators: DecoratorNode[] | null): void { + if (decorators && decorators.length > 0) { + this.diagnostics.error( + DiagnosticCode.Decorators_are_not_valid_here, + Range.join(decorators[0].range, decorators[decorators.length - 1].range) + ); + } + } +} + export namespace OperatorKind { /** Returns the operator kind represented by the specified decorator and string argument. */ @@ -462,6 +492,8 @@ export class Program extends DiagnosticEmitter { nextSignatureId: i32 = 0; /** An indicator if the program has been initialized. */ initialized: bool = false; + /** Indicates whether the one-shot post-transform parameter decorator validation has run. */ + parameterDecoratorsValidated: bool = false; // Lookup maps @@ -901,7 +933,10 @@ export class Program extends DiagnosticEmitter { Node.createSimpleTypeName(CommonNames.void_, range), null, false, range ), - null, false, range + null, + null, + false, + range ); } return Node.createFunctionDeclaration( @@ -1497,6 +1532,29 @@ export class Program extends DiagnosticEmitter { } } + /** Rejects parameter decorators that survive transform time. These remain transform-only syntax. */ + validateParameterDecorators(): void { + if (this.parameterDecoratorsValidated) return; + this.parameterDecoratorsValidated = true; + let validator = new ParameterDecoratorValidator(this); + let sources = this.sources; + for (let i = 0, k = sources.length; i < k; ++i) { + let source = sources[i]; + let parameterDecoratorStatements = source.parameterDecoratorStatements; + if (!parameterDecoratorStatements) continue; + let statements = source.statements; + for (let j = 0, l = parameterDecoratorStatements.length; j < l; ++j) { + let statement = parameterDecoratorStatements[j]; + for (let m = 0, n = statements.length; m < n; ++m) { + if (statements[m] == statement) { + validator.visitNode(statement); + break; + } + } + } + } + } + /** Processes overridden members by this class in a base class. */ private processOverrides( thisPrototype: ClassPrototype, @@ -2703,6 +2761,7 @@ export class Program extends DiagnosticEmitter { [], typeNode, null, + null, false, declaration.range ), @@ -2725,11 +2784,13 @@ export class Program extends DiagnosticEmitter { declaration.name, typeNode, null, + null, declaration.name.range ) ], Node.createOmittedType(declaration.name.range.atEnd), null, + null, false, declaration.range ), @@ -4027,7 +4088,7 @@ export class PropertyPrototype extends DeclaredElement { fieldDeclaration.decorators, fieldDeclaration.flags | CommonFlags.Instance | CommonFlags.Get, null, - new FunctionTypeNode([], typeNode, null, false, nativeRange), + new FunctionTypeNode([], typeNode, null, null, false, nativeRange), null, nativeRange ); @@ -4041,7 +4102,10 @@ export class PropertyPrototype extends DeclaredElement { new ParameterNode( ParameterKind.Default, fieldDeclaration.name, - typeNode, null, nativeRange + typeNode, + null, + null, + nativeRange ) ], new NamedTypeNode( @@ -4051,7 +4115,10 @@ export class PropertyPrototype extends DeclaredElement { ), null, false, nativeRange ), - null, false, nativeRange + null, + null, + false, + nativeRange ), null, nativeRange ); diff --git a/tests/compiler/parameter-decorators-errors.json b/tests/compiler/parameter-decorators-errors.json new file mode 100644 index 0000000000..7d0dc11547 --- /dev/null +++ b/tests/compiler/parameter-decorators-errors.json @@ -0,0 +1,22 @@ +{ + "asc_flags": [ + ], + "stderr": [ + "TS1003: Identifier expected.", + "function regularEmpty(@first): void {}", + "TS1003: Identifier expected.", + "function firstEmpty(@first, value: i32): void {}", + "TS1206: Decorators are not valid here.", + "function regularWrongPos(value @first: i32): void {}", + "TS1206: Decorators are not valid here.", + "function regularWrongPos2(value: i32 @first): void {}", + "TS1206: Decorators are not valid here.", + "function restWrongPos(...values @rest: i32[]): void {}", + "TS1206: Decorators are not valid here.", + "function selfWrongPos(this: i32 @self): void {}", + "TS1003: Identifier expected.", + "function noLits(@123 value: i32): void {}", + "TS1003: Identifier expected.", + "function noExprs(@(a + b) value: i32): void {}" + ] +} diff --git a/tests/compiler/parameter-decorators-errors.ts b/tests/compiler/parameter-decorators-errors.ts new file mode 100644 index 0000000000..f60d315dd8 --- /dev/null +++ b/tests/compiler/parameter-decorators-errors.ts @@ -0,0 +1,8 @@ +function regularEmpty(@first): void {} +function firstEmpty(@first, value: i32): void {} +function regularWrongPos(value @first: i32): void {} +function regularWrongPos2(value: i32 @first): void {} +function restWrongPos(...values @rest: i32[]): void {} +function selfWrongPos(this: i32 @self): void {} +function noLits(@123 value: i32): void {} +function noExprs(@(a + b) value: i32): void {} diff --git a/tests/compiler/parameter-decorators.json b/tests/compiler/parameter-decorators.json new file mode 100644 index 0000000000..f5f06aabaf --- /dev/null +++ b/tests/compiler/parameter-decorators.json @@ -0,0 +1,25 @@ +{ + "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 => {};", + "TS1206: Decorators are not valid here.", + "export function nested(@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..23d97852fe --- /dev/null +++ b/tests/compiler/parameter-decorators.ts @@ -0,0 +1,17 @@ +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 => {}; +namespace ns { + export function nested(@arg value: i32): void {} +} + +ERROR("EOF"); diff --git a/tests/parser/parameter-decorators.ts b/tests/parser/parameter-decorators.ts new file mode 100644 index 0000000000..5e02463e5a --- /dev/null +++ b/tests/parser/parameter-decorators.ts @@ -0,0 +1,14 @@ +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 => {}; +namespace ns { + export function nested(@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..2250edd292 --- /dev/null +++ b/tests/parser/parameter-decorators.ts.fixture.ts @@ -0,0 +1,14 @@ +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 => {}; +namespace ns { + export function nested(@arg value: i32): void {} +} diff --git a/tests/transform/cjs/remove-parameter-decorators.js b/tests/transform/cjs/remove-parameter-decorators.js new file mode 100644 index 0000000000..2eee0745c3 --- /dev/null +++ b/tests/transform/cjs/remove-parameter-decorators.js @@ -0,0 +1,329 @@ +// 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 = { + 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..5064593bf8 --- /dev/null +++ b/tests/transform/parameter-decorators.ts @@ -0,0 +1,14 @@ +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 => {}; +namespace ns { + export function nested(@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..046cc5b67e --- /dev/null +++ b/tests/transform/remove-parameter-decorators.js @@ -0,0 +1,329 @@ +// 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 = { + 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); +}