diff --git a/NOTICE b/NOTICE index 148bc0ca87..f47078b73c 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/TODO.md b/TODO.md new file mode 100644 index 0000000000..4c8c6ce203 --- /dev/null +++ b/TODO.md @@ -0,0 +1,19 @@ +Working document of notes/tasks for trying to figure out how to correctly add decorators to AssemblyScript. Will be delete once the PR is complete. + + +- [ ] Ask claude to explain how the AssemblyScript parser works and try to add a garbage throwaway feature myself manually for learning purposes (eg. `mut` keyword for syntax like `mut myVariable = 10`). + +- [ ] After implementing re-read all the stuff dcode and Max have been explaining and try to better understand it + +- [ ] Need to add a transform hook for valdiating after parse but before compiliation. + +- [ ] Need to add a test case for having a transform validate decorators. + +NOTES: +- AST can be extended without breaking changes, this is perfectly fine and anything that is added will just be ignored by the compiler but at least transformer plugins can then make use of it. No need to validate and throw errors unless it is an AST parse error. + - Need to add a transformer hook so transformers can add their own validation. This way AssemblyScript can delegate validation to transformers instead of having to handle it internally, which doesn't make sense because AS shouldn't be in charge of worring about that in the first place. + +- "Transformer" refers the to transformers in /tests/transform + +- Rather than have a method validate the decorators, they should just be part of the AST. Once the AST can directly handle the decorators then validating decorators syntax will happen automatically. This also means we need actual AST nodes in ast.ts for the different parts of decorators rather than just inlining the decorators. +- Correction to the above. The method validation was only 1 part of the problem, the compiler of assemblyscript is for outputting WASM binaries not validating valid AST syntax. The compiler.ts will not need to be touched AT ALL for this PR. \ No newline at end of file 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..83b593a661 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 01d8e9a421..f44bbf5834 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 as transform-only syntax. */ + 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 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. */ @@ -1664,6 +1669,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/compiler.ts b/src/compiler.ts index 7e4c3c9c40..95de3988d5 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -536,8 +536,7 @@ export class Compiler extends DiagnosticEmitter { // initialize lookup maps, built-ins, imports, exports, etc. this.program.initialize(); - - + // Binaryen treats all function references as being leaked to the outside world when // the module isn't marked as closed-world (see WebAssembly/binaryen#7135). Therefore, // we should mark the module as closed-world when we're definitely sure it is. 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/parser.ts b/src/parser.ts index 7c69843973..52e6edb5fc 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -202,15 +202,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 @@ -429,7 +423,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( @@ -698,17 +693,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; @@ -723,6 +720,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; @@ -744,7 +748,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 +779,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 +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, decorators, tn.range(paramStart, tn.pos)); if (!parameters) parameters = [ param ]; else parameters.push(param); this.error( @@ -838,6 +846,7 @@ export class Parser extends DiagnosticEmitter { firstParamNameNoType, Node.createOmittedType(firstParamNameNoType.range.atEnd), null, + null, firstParamNameNoType.range ); if (!parameters) parameters = [ param ]; @@ -869,17 +878,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 { @@ -924,6 +957,33 @@ 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 hasDecorators = signature.explicitThisDecorators != null && signature.explicitThisDecorators.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, @@ -1226,14 +1286,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; @@ -1241,42 +1304,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( @@ -1305,6 +1380,7 @@ export class Parser extends DiagnosticEmitter { } } parameters.push(param); + first = false; if (!tn.skip(Token.Comma)) { if (tn.skip(Token.CloseParen)) { break; @@ -1322,24 +1398,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 +1441,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 +1457,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 +1493,7 @@ export class Parser extends DiagnosticEmitter { identifier, type, initializer, + decorators, Range.join(assert(startRange), tn.range()) ); param.flags |= accessFlags; @@ -1520,9 +1603,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 +1682,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 +1699,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 +1729,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) { @@ -1882,14 +1979,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, @@ -2279,9 +2371,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 +3823,7 @@ export class Parser extends DiagnosticEmitter { Node.createEmptyIdentifierExpression(tn.range(startPos)), [], null, + null, ArrowKind.Parenthesized ); } @@ -3738,6 +3833,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 +4022,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..66087d7b79 100644 --- a/src/program.ts +++ b/src/program.ts @@ -111,7 +111,7 @@ import { VariableStatement, ParameterKind, ParameterNode, - TypeName + TypeName, } from "./ast"; import { @@ -462,6 +462,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 +903,10 @@ export class Program extends DiagnosticEmitter { Node.createSimpleTypeName(CommonNames.void_, range), null, false, range ), - null, false, range + null, + null, + false, + range ); } return Node.createFunctionDeclaration( @@ -2703,6 +2708,7 @@ export class Program extends DiagnosticEmitter { [], typeNode, null, + null, false, declaration.range ), @@ -2725,11 +2731,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 +4035,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 +4049,10 @@ export class PropertyPrototype extends DeclaredElement { new ParameterNode( ParameterKind.Default, fieldDeclaration.name, - typeNode, null, nativeRange + typeNode, + null, + null, + nativeRange ) ], new NamedTypeNode( @@ -4051,7 +4062,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..d2a6abbb66 --- /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 before compilation rejects 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..deacb91352 --- /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 before compilation rejects 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; + } + } + } +}