diff --git a/NOTICE b/NOTICE index da2cb563d4..03e5b40508 100644 --- a/NOTICE +++ b/NOTICE @@ -63,6 +63,7 @@ under the licensing terms detailed in LICENSE: * Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> * EDM115 * Weixie Cui +* Abdiel Lopez Portions of this software are derived from third-party works licensed under the following terms: diff --git a/cli/index.d.ts b/cli/index.d.ts index 58c7fb4ef6..a0e2da1113 100644 --- a/cli/index.d.ts +++ b/cli/index.d.ts @@ -273,6 +273,11 @@ export abstract class Transform { /** Lists all files in a directory. */ listFiles(dirname: string, baseDir: string): (string[] | null) | Promise; + /** Called after program initialization, before WASM compilation. Transformers should use + * this hook to perform custom validation of AST constructs such as parameter decorators + * and emit their own diagnostics. Decorators remain in the AST unchanged. */ + beforeCompile?(program: Program): void | Promise; + /** Called when parsing is complete, before a program is instantiated from the AST. */ afterParse?(parser: Parser): void | Promise; 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 47b23f9761..0f8f828e1a 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 --experimental-strip-types --no-warnings bin/asc tests/transform/parameter-decorators.ts --transform ./tests/transform/remove-parameter-decorators.ts --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 da649de549..05b69c0347 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -156,10 +156,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 createTupleType( @@ -191,9 +192,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 @@ -935,6 +937,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 as transform-only syntax. */ + public explicitThisDecorators: DecoratorNode[] | null, /** Whether nullable or not. */ isNullable: bool, /** Source range. */ @@ -997,12 +1001,13 @@ export class ParameterNode extends Node { public type: TypeNode, /** Initializer expression, if any. */ public initializer: Expression | null, + /** Decorators, if any. Preserved as transform-only syntax so transforms can rewrite or remove 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. */ @@ -1696,6 +1701,8 @@ export class Source extends Node { debugInfoIndex: i32 = -1; /** Re-exported sources. */ exportPaths: string[] | null = null; + /** Function types with parameter or this-parameter decorators, revisited after transforms for validation. */ + decoratedFunctionTypes: FunctionTypeNode[] | null = null; /** Checks if this source represents native code. */ get isNative(): bool { diff --git a/src/extra/ast.ts b/src/extra/ast.ts index 699b2e0c71..50ebff000e 100644 --- a/src/extra/ast.ts +++ b/src/extra/ast.ts @@ -436,6 +436,7 @@ export class ASTBuilder { sb.push(isNullable ? "((" : "("); let explicitThisType = node.explicitThisType; if (explicitThisType) { + this.serializeParameterDecorators(node.explicitThisDecorators); sb.push("this: "); this.visitTypeNode(explicitThisType); } @@ -1189,6 +1190,7 @@ export class ASTBuilder { let numParameters = parameters.length; let explicitThisType = signature.explicitThisType; if (explicitThisType) { + this.serializeParameterDecorators(signature.explicitThisDecorators); sb.push("this: "); this.visitTypeNode(explicitThisType); } @@ -1577,7 +1579,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); @@ -1592,16 +1594,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/parser.ts b/src/parser.ts index 157351cd3a..bcd4d56b79 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -207,15 +207,9 @@ export class Parser extends DiagnosticEmitter { // check decorators let decorators: DecoratorNode[] | null = null; - while (tn.skip(Token.At)) { - if (startPos < 0) startPos = tn.tokenPos; - let decorator = this.parseDecorator(tn); - if (!decorator) { - this.skipStatement(tn); - continue; - } - if (!decorators) decorators = [decorator]; - else decorators.push(decorator); + if (tn.peek() == Token.At) { + startPos = tn.nextTokenPos; + decorators = this.parseDecorators(tn, true); } // check modifiers @@ -434,7 +428,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( @@ -746,17 +741,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 leading parameter decorators for transforms while still reporting misplaced ones. */ 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; @@ -771,6 +768,13 @@ export class Parser extends DiagnosticEmitter { do { let paramStart = -1; let kind = ParameterKind.Default; + // Preserve leading parameter decorators in the AST so transforms can inspect or remove them later. + let decorators = this.parseDecorators(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; @@ -792,7 +796,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; @@ -821,10 +827,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; @@ -832,7 +840,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( @@ -886,6 +894,7 @@ export class Parser extends DiagnosticEmitter { firstParamNameNoType, Node.createOmittedType(firstParamNameNoType.range.atEnd), null, + null, firstParamNameNoType.range ); if (!parameters) parameters = [ param ]; @@ -917,17 +926,41 @@ 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 + /** Parses zero or more decorators starting at the current token. */ + private parseDecorators( + tn: Tokenizer, + skipStatementOnError: bool = false + ): DecoratorNode[] | null { + let decorators: DecoratorNode[] | null = null; + while (tn.skip(Token.At)) { + let decorator = this.parseDecorator(tn); + if (!decorator) { + if (skipStatementOnError) { + this.skipStatement(tn); + continue; + } + break; + } + if (!decorators) decorators = [decorator]; + else decorators.push(decorator); + } + return decorators; + } + parseDecorator( tn: Tokenizer ): DecoratorNode | null { @@ -972,6 +1005,34 @@ export class Parser extends DiagnosticEmitter { return null; } + /** Records a function type that has parameter or this-parameter decorators for post-transform validation. */ + private noteFunctionTypeParameterDecorators(signature: FunctionTypeNode): void { + let thisDecorators = signature.explicitThisDecorators; + let hasDecorators = thisDecorators != null && thisDecorators.length > 0; + if (!hasDecorators) { + let params = signature.parameters; + for (let i = 0, k = params.length; i < k; ++i) { + if (params[i].decorators != null) { hasDecorators = true; break; } + } + } + if (!hasDecorators) return; + let source = signature.range.source; + let tracked = source.decoratedFunctionTypes; + if (!tracked) source.decoratedFunctionTypes = [signature]; + else tracked.push(signature); + } + + /** Consumes misplaced parameter decorators after a parameter has already started so they diagnose as TS1206 instead of cascading. */ + private tryParseParameterDecorators(tn: Tokenizer): void { + let decorators = this.parseDecorators(tn); + if (decorators) { + this.error( + DiagnosticCode.Decorators_are_not_valid_here, + Range.join(decorators[0].range, decorators[decorators.length - 1].range) + ); + } + } + parseVariable( tn: Tokenizer, flags: CommonFlags, @@ -1274,14 +1335,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 captured by the current parseParameters call. Preserved as transform-only syntax. */ + 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; @@ -1289,42 +1353,54 @@ 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; + + // Preserve leading parameter decorators in the AST so transforms can inspect or remove them later. + let paramDecorators = this.parseDecorators(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( @@ -1353,6 +1429,7 @@ export class Parser extends DiagnosticEmitter { } } parameters.push(param); + first = false; if (!tn.skip(Token.Comma)) { if (tn.skip(Token.CloseParen)) { break; @@ -1370,24 +1447,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) { @@ -1409,12 +1490,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)) { @@ -1425,12 +1506,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) { @@ -1459,6 +1542,7 @@ export class Parser extends DiagnosticEmitter { identifier, type, initializer, + decorators, Range.join(assert(startRange), tn.range()) ); param.flags |= accessFlags; @@ -1568,9 +1652,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)) { @@ -1645,7 +1731,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( @@ -1653,6 +1748,7 @@ export class Parser extends DiagnosticEmitter { name: IdentifierExpression, parameters: ParameterNode[], explicitThis: NamedTypeNode | null, + explicitThisDecorators: DecoratorNode[] | null, arrowKind: ArrowKind, startPos: i32 = -1, signatureStart: i32 = -1 @@ -1682,9 +1778,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) { @@ -1930,14 +2028,9 @@ export class Parser extends DiagnosticEmitter { let isInterface = parent.kind == NodeKind.InterfaceDeclaration; let startPos = 0; let decorators: DecoratorNode[] | null = null; - if (tn.skip(Token.At)) { - startPos = tn.tokenPos; - do { - let decorator = this.parseDecorator(tn); - if (!decorator) break; - if (!decorators) decorators = new Array(); - decorators.push(decorator); - } while (tn.skip(Token.At)); + if (tn.peek() == Token.At) { + startPos = tn.nextTokenPos; + decorators = this.parseDecorators(tn); if (isInterface && decorators) { this.error( DiagnosticCode.Decorators_are_not_valid_here, @@ -2327,9 +2420,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)) { @@ -3777,6 +3872,7 @@ export class Parser extends DiagnosticEmitter { Node.createEmptyIdentifierExpression(tn.range(startPos)), [], null, + null, ArrowKind.Parenthesized ); } @@ -3786,6 +3882,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); @@ -3974,10 +4071,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 f5d9b7cae6..e5e99a02ff 100644 --- a/src/program.ts +++ b/src/program.ts @@ -111,7 +111,7 @@ import { VariableStatement, ParameterKind, ParameterNode, - TypeName + TypeName, } from "./ast"; import { @@ -904,7 +904,10 @@ export class Program extends DiagnosticEmitter { Node.createSimpleTypeName(CommonNames.void_, range), null, false, range ), - null, false, range + null, + null, + false, + range ); } return Node.createFunctionDeclaration( @@ -2720,6 +2723,7 @@ export class Program extends DiagnosticEmitter { [], typeNode, null, + null, false, declaration.range ), @@ -2742,11 +2746,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 ), @@ -4044,7 +4050,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 ); @@ -4058,7 +4064,10 @@ export class PropertyPrototype extends DeclaredElement { new ParameterNode( ParameterKind.Default, fieldDeclaration.name, - typeNode, null, nativeRange + typeNode, + null, + null, + nativeRange ) ], new NamedTypeNode( @@ -4068,7 +4077,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.debug.wat b/tests/compiler/parameter-decorators.debug.wat new file mode 100644 index 0000000000..70a80ed9fd --- /dev/null +++ b/tests/compiler/parameter-decorators.debug.wat @@ -0,0 +1,18 @@ +(module + (type $0 (func (param i32))) + (global $parameter-decorators/expression i32 (i32.const 32)) + (global $parameter-decorators/arrow i32 (i32.const 64)) + (global $~lib/memory/__data_end i32 (i32.const 76)) + (global $~lib/memory/__stack_pointer (mut i32) (i32.const 32844)) + (global $~lib/memory/__heap_base i32 (i32.const 32844)) + (memory $0 1) + (data $0 (i32.const 12) "\1c\00\00\00\00\00\00\00\00\00\00\00\04\00\00\00\08\00\00\00\01\00\00\00\00\00\00\00\00\00\00\00") + (data $1 (i32.const 44) "\1c\00\00\00\00\00\00\00\00\00\00\00\04\00\00\00\08\00\00\00\02\00\00\00\00\00\00\00\00\00\00\00") + (table $0 3 3 funcref) + (elem $0 (i32.const 1) $start:parameter-decorators~anonymous|0 $start:parameter-decorators~anonymous|1) + (export "memory" (memory $0)) + (func $start:parameter-decorators~anonymous|0 (param $value i32) + ) + (func $start:parameter-decorators~anonymous|1 (param $value i32) + ) +) diff --git a/tests/compiler/parameter-decorators.json b/tests/compiler/parameter-decorators.json new file mode 100644 index 0000000000..1bdd02b1be --- /dev/null +++ b/tests/compiler/parameter-decorators.json @@ -0,0 +1,4 @@ +{ + "asc_flags": [ + ] +} diff --git a/tests/compiler/parameter-decorators.release.wat b/tests/compiler/parameter-decorators.release.wat new file mode 100644 index 0000000000..4f6905b22a --- /dev/null +++ b/tests/compiler/parameter-decorators.release.wat @@ -0,0 +1,8 @@ +(module + (memory $0 1) + (data $0 (i32.const 1036) "\1c") + (data $0.1 (i32.const 1048) "\04\00\00\00\08\00\00\00\01") + (data $1 (i32.const 1068) "\1c") + (data $1.1 (i32.const 1080) "\04\00\00\00\08\00\00\00\02") + (export "memory" (memory $0)) +) diff --git a/tests/compiler/parameter-decorators.ts b/tests/compiler/parameter-decorators.ts new file mode 100644 index 0000000000..5f4a9b22c4 --- /dev/null +++ b/tests/compiler/parameter-decorators.ts @@ -0,0 +1,16 @@ +function regular(@first value: i32): void {} +function multiParam(@first huh: string, @second(123, 456) works: i32, @third("can pass a string?") hello: string): 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 {} +} 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..a892296494 --- /dev/null +++ b/tests/transform/cjs/remove-parameter-decorators.js @@ -0,0 +1,17 @@ +// Example transform proving that preserved parameter decorators can be stripped +// during afterInitialize, since the compiler otherwise ignores them. +console.log("CommonJS parameter decorator removal transform loaded"); + +exports.afterInitialize = (program) => { + console.log("- afterInitialize strip parameter decorators"); + for (const source of program.sources) { + const fts = source.decoratedFunctionTypes; + if (!fts) continue; + for (const ft of fts) { + ft.explicitThisDecorators = null; + for (const param of ft.parameters) { + param.decorators = null; + } + } + } +}; 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.ts b/tests/transform/remove-parameter-decorators.ts new file mode 100644 index 0000000000..f555e3768a --- /dev/null +++ b/tests/transform/remove-parameter-decorators.ts @@ -0,0 +1,19 @@ +// Example transform proving that preserved parameter decorators can be stripped +// during afterInitialize, since the compiler otherwise ignores them. +console.log("Parameter decorator removal transform loaded"); + +import type { Program } from "assemblyscript"; + +export function afterInitialize(program: Program): void { + console.log("- afterInitialize strip parameter decorators"); + for (const source of program.sources) { + const fts = source.decoratedFunctionTypes; + if (!fts) continue; + for (const ft of fts) { + ft.explicitThisDecorators = null; + for (const param of ft.parameters) { + param.decorators = null; + } + } + } +}