Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .chronus/changes/reserve-keywords-2025-2-12-22-22-24.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: breaking
packages:
- "@typespec/compiler"
---

Adding new keywords for future use:
- `statemachine`
- `macro`
- `package`
- `metadata`
- `env`
- `arg`
- `declare`
- `array`
- `struct`
- `record`
- `module`
- `trait`
- `this`
- `self`
- `super`
- `keyof`
- `with`
- `implements`
- `impl`
- `satisfies`
- `flag`
- `auto`
- `partial`
- `private`
- `public`
- `protected`
- `internal`
- `sealed`
- `local`
- `async`
10 changes: 10 additions & 0 deletions .chronus/changes/reserve-keywords-2025-2-13-1-29-22.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: internal
packages:
- "@typespec/http-specs"
- "@typespec/openapi3"
- "@typespec/protobuf"
---

Reserve keywords
14 changes: 10 additions & 4 deletions packages/compiler/src/core/helpers/syntax-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CharCode, isIdentifierContinue, isIdentifierStart, utf16CodeUnits } from "../charcode.js";
import { Keywords } from "../scanner.js";
import { Keywords, ReservedKeywords } from "../scanner.js";
import { IdentifierNode, MemberExpressionNode, SyntaxKind, TypeReferenceNode } from "../types.js";

/**
Expand All @@ -13,8 +13,11 @@ import { IdentifierNode, MemberExpressionNode, SyntaxKind, TypeReferenceNode } f
* printIdentifier("foo bar") // `foo bar`
* ```
*/
export function printIdentifier(sv: string) {
if (needBacktick(sv)) {
export function printIdentifier(
sv: string,
/** @internal */ context: "allow-reserved" | "disallow-reserved" = "disallow-reserved",
) {
if (needBacktick(sv, context)) {
const escapedString = sv
.replace(/\\/g, "\\\\")
.replace(/\n/g, "\\n")
Expand All @@ -27,10 +30,13 @@ export function printIdentifier(sv: string) {
}
}

function needBacktick(sv: string) {
function needBacktick(sv: string, context: "allow-reserved" | "disallow-reserved"): boolean {
if (sv.length === 0) {
return false;
}
if (context === "allow-reserved" && ReservedKeywords.has(sv)) {
return false;
}
if (Keywords.has(sv)) {
return true;
}
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/core/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ const diagnostics = {
severity: "error",
messages: {
default: "Keyword cannot be used as identifier.",
future: paramMessage`${"name"} is a reserved keyword`,
},
},
"invalid-directive-location": {
Expand Down
73 changes: 58 additions & 15 deletions packages/compiler/src/core/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
isComment,
isKeyword,
isPunctuation,
isReservedKeyword,
isStatementKeyword,
isTrivia,
skipContinuousIdentifier,
Expand Down Expand Up @@ -737,8 +738,29 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
};
}

function parseIdOrValueForVariant(): Expression {
const nextToken = token();

let id: IdentifierNode | undefined;
if (isReservedKeyword(nextToken)) {
id = parseIdentifier({ allowReservedIdentifier: true });
// If the next token is not a colon this means we tried to use the reserved keyword as a type reference
if (token() !== Token.Colon) {
error({ code: "reserved-identifier", messageId: "future", format: { name: id.sv } });
}
return {
kind: SyntaxKind.TypeReference,
target: id,
arguments: [],
...finishNode(id.pos),
};
} else {
return parseExpression();
}
}

function parseUnionVariant(pos: number, decorators: DecoratorExpressionNode[]): UnionVariantNode {
const idOrExpr = parseExpression();
const idOrExpr = parseIdOrValueForVariant();
if (parseOptional(Token.Colon)) {
let id: IdentifierNode | undefined = undefined;

Expand Down Expand Up @@ -785,7 +807,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa

function parseUsingStatement(pos: number): UsingStatementNode {
parseExpected(Token.UsingKeyword);
const name = parseIdentifierOrMemberExpression(undefined, true);
const name = parseIdentifierOrMemberExpression();
parseExpected(Token.Semicolon);

return {
Expand Down Expand Up @@ -1010,6 +1032,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
const id = parseIdentifier({
message: "property",
allowStringLiteral: true,
allowReservedIdentifier: true,
});

const optional = parseOptional(Token.Question);
Expand Down Expand Up @@ -1056,6 +1079,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
function parseObjectLiteralProperty(pos: number): ObjectLiteralPropertyNode {
const id = parseIdentifier({
message: "property",
allowReservedIdentifier: true,
});

parseExpected(Token.Colon);
Expand Down Expand Up @@ -1170,6 +1194,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
const id = parseIdentifier({
message: "enumMember",
allowStringLiteral: true,
allowReservedIdentifier: true,
});

let value: StringLiteralNode | NumericLiteralNode | undefined;
Expand Down Expand Up @@ -1366,15 +1391,21 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
message?: keyof CompilerDiagnostics["token-expected"],
): TypeReferenceNode {
const pos = tokenPos();
const target = parseIdentifierOrMemberExpression(message);
const target = parseIdentifierOrMemberExpression({
message,
allowReservedIdentifierInMember: true,
});
return parseReferenceExpressionInternal(target, pos);
}

function parseCallOrReferenceExpression(
message?: keyof CompilerDiagnostics["token-expected"],
): TypeReferenceNode | CallExpressionNode {
const pos = tokenPos();
const target = parseIdentifierOrMemberExpression(message);
const target = parseIdentifierOrMemberExpression({
message,
allowReservedIdentifierInMember: true,
});
if (token() === Token.OpenParen) {
const { items: args } = parseList(ListKind.FunctionArguments, parseExpression);
return {
Expand Down Expand Up @@ -1451,7 +1482,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
// identifier. We want to parse `@ model Foo` as invalid decorator
// `@<missing identifier>` applied to `model Foo`, and not as `@model`
// applied to invalid statement `Foo`.
const target = parseIdentifierOrMemberExpression(undefined, false);
const target = parseIdentifierOrMemberExpression({ allowReservedIdentifierInMember: true });
const { items: args } = parseOptionalList(ListKind.DecoratorArguments, parseExpression);
if (args.length === 0) {
error({ code: "augment-decorator-target" });
Expand Down Expand Up @@ -1513,7 +1544,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
// identifier. We want to parse `@ model Foo` as invalid decorator
// `@<missing identifier>` applied to `model Foo`, and not as `@model`
// applied to invalid statement `Foo`.
const target = parseIdentifierOrMemberExpression(undefined, false);
const target = parseIdentifierOrMemberExpression({
allowReservedIdentifier: true,
allowReservedIdentifierInMember: true,
});
const { items: args } = parseOptionalList(ListKind.DecoratorArguments, parseExpression);
return {
kind: SyntaxKind.DecoratorExpression,
Expand Down Expand Up @@ -1581,14 +1615,16 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
}
}

function parseIdentifierOrMemberExpression(
message?: keyof CompilerDiagnostics["token-expected"],
recoverFromKeyword = true,
): IdentifierNode | MemberExpressionNode {
function parseIdentifierOrMemberExpression(options?: {
message?: keyof CompilerDiagnostics["token-expected"];
// Temporary solution see doc on parseIdentifier
allowReservedIdentifier?: boolean;
allowReservedIdentifierInMember?: boolean;
}): IdentifierNode | MemberExpressionNode {
const pos = tokenPos();
let base: IdentifierNode | MemberExpressionNode = parseIdentifier({
message,
recoverFromKeyword,
message: options?.message,
allowReservedIdentifier: options?.allowReservedIdentifier,
});
while (token() !== Token.EndOfFile) {
if (parseOptional(Token.Dot)) {
Expand All @@ -1601,7 +1637,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
// `@Outer.model` applied to invalid statement `M {}` instead of
// having incomplete decorator `@Outer.` applied to `model M {}`.
id: parseIdentifier({
recoverFromKeyword: false,
allowReservedIdentifier: options?.allowReservedIdentifierInMember,
}),
selector: ".",
...finishNode(pos),
Expand Down Expand Up @@ -1912,10 +1948,17 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa
function parseIdentifier(options?: {
message?: keyof CompilerDiagnostics["token-expected"];
allowStringLiteral?: boolean; // Allow string literals to be used as identifiers for backward-compatibility, but convert to an identifier node.
recoverFromKeyword?: boolean;

// Temporary solution to allow reserved keywords as identifiers in certain contexts. This should get expanded to a more general solution per keyword category.
allowReservedIdentifier?: boolean;
}): IdentifierNode {
if (options?.recoverFromKeyword !== false && isKeyword(token())) {
if (isKeyword(token())) {
error({ code: "reserved-identifier" });
return createMissingIdentifier();
} else if (isReservedKeyword(token())) {
if (!options?.allowReservedIdentifier) {
error({ code: "reserved-identifier", messageId: "future", format: { name: tokenValue() } });
}
} else if (
token() !== Token.Identifier &&
(!options?.allowStringLiteral || token() !== Token.StringLiteral)
Expand Down
Loading