From 475c1da2ce3564922269056f92e350452e29fb1c Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 9 Oct 2025 15:12:17 -0400 Subject: [PATCH 01/14] First stab --- grammars/typespec.json | 70 +++--- packages/compiler/src/core/binder.ts | 76 +++++-- packages/compiler/src/core/checker.ts | 83 +++++++- packages/compiler/src/core/messages.ts | 1 + packages/compiler/src/core/modifiers.ts | 74 +++++++ packages/compiler/src/core/name-resolver.ts | 3 + packages/compiler/src/core/parser.ts | 199 ++++++++++++------ packages/compiler/src/core/source-loader.ts | 3 + packages/compiler/src/core/types.ts | 52 +++-- .../compiler/src/formatter/print/printer.ts | 2 + packages/compiler/src/server/tmlanguage.ts | 2 +- packages/compiler/test/binder.test.ts | 3 + packages/compiler/test/name-resolver.test.ts | 3 + 13 files changed, 427 insertions(+), 144 deletions(-) create mode 100644 packages/compiler/src/core/modifiers.ts diff --git a/grammars/typespec.json b/grammars/typespec.json index 9f4a2783b60..c1a46162808 100644 --- a/grammars/typespec.json +++ b/grammars/typespec.json @@ -19,7 +19,7 @@ "name": "keyword.operator.assignment.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#expression" @@ -37,7 +37,7 @@ "name": "entity.name.type.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#alias-id" @@ -58,7 +58,7 @@ "name": "entity.name.tag.tsp" } }, - "end": "(?=([_$[:alpha:]]|`))|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=([_$[:alpha:]]|`))|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -117,7 +117,7 @@ "name": "variable.name.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#type-annotation" @@ -141,7 +141,7 @@ "name": "entity.name.tag.tsp" } }, - "end": "(?=([_$[:alpha:]]|`))|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=([_$[:alpha:]]|`))|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -165,7 +165,7 @@ "name": "entity.name.function.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -183,7 +183,7 @@ "name": "keyword.directive.name.tsp" } }, - "end": "$|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "$|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#string-literal" @@ -308,7 +308,7 @@ "name": "keyword.operator.type.annotation.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -329,7 +329,7 @@ "name": "entity.name.type.tsp" } }, - "end": "(?<=\\})|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?<=\\})|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -397,7 +397,7 @@ "name": "entity.name.function.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -422,7 +422,7 @@ "name": "keyword.other.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -469,7 +469,7 @@ "name": "keyword.other.tsp" } }, - "end": "((?=\\{)|(?=;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b))", + "end": "((?=\\{)|(?=;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b))", "patterns": [ { "include": "#expression" @@ -490,7 +490,7 @@ "name": "entity.name.function.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -508,7 +508,7 @@ "name": "keyword.other.tsp" } }, - "end": "(?<=\\})|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?<=\\})|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -574,7 +574,7 @@ "name": "keyword.other.tsp" } }, - "end": "((?=\\{)|(?=;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b))", + "end": "((?=\\{)|(?=;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b))", "patterns": [ { "include": "#expression" @@ -595,7 +595,7 @@ "name": "string.quoted.double.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -619,7 +619,7 @@ "name": "keyword.other.tsp" } }, - "end": "(?<=\\})|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?<=\\})|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -658,7 +658,7 @@ "namespace-name": { "name": "meta.namespace-name.typespec", "begin": "(?=([_$[:alpha:]]|`))", - "end": "((?=\\{)|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b))", + "end": "((?=\\{)|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b))", "patterns": [ { "include": "#identifier-expression" @@ -676,7 +676,7 @@ "name": "keyword.other.tsp" } }, - "end": "((?<=\\})|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b))", + "end": "((?<=\\})|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b))", "patterns": [ { "include": "#token" @@ -736,7 +736,7 @@ "name": "keyword.operator.type.annotation.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -754,7 +754,7 @@ "name": "keyword.other.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#expression" @@ -820,7 +820,7 @@ "name": "entity.name.function.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -909,7 +909,7 @@ "name": "entity.name.function.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -927,7 +927,7 @@ "name": "keyword.other.tsp" } }, - "end": "(?=;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#expression" @@ -948,7 +948,7 @@ "name": "entity.name.type.tsp" } }, - "end": "(?<=\\})|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?<=\\})|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -972,7 +972,7 @@ "name": "keyword.operator.spread.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#expression" @@ -1180,7 +1180,7 @@ "name": "keyword.operator.assignment.tsp" } }, - "end": "(?=>)|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=>)|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "endCaptures": { "0": { "name": "keyword.operator.assignment.tsp" @@ -1232,7 +1232,7 @@ "name": "entity.name.type.tsp" } }, - "end": "(?=>)|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=>)|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -1253,7 +1253,7 @@ "name": "keyword.other.tsp" } }, - "end": "(?=>)|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=>)|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#expression" @@ -1268,7 +1268,7 @@ "name": "keyword.operator.assignment.tsp" } }, - "end": "(?=>)|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=>)|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#expression" @@ -1306,7 +1306,7 @@ "name": "keyword.other.tsp" } }, - "end": "(?=>)|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=>)|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#expression" @@ -1359,7 +1359,7 @@ "name": "entity.name.type.tsp" } }, - "end": "(?<=\\})|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?<=\\})|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -1380,7 +1380,7 @@ "name": "keyword.operator.type.annotation.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -1398,7 +1398,7 @@ "name": "keyword.other.tsp" } }, - "end": "(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -1419,7 +1419,7 @@ "name": "keyword.other.tsp" } }, - "end": "(?=>)|(?=,|;|@|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=>)|(?=,|;|@|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#expression" diff --git a/packages/compiler/src/core/binder.ts b/packages/compiler/src/core/binder.ts index 906218f99b5..003963dffea 100644 --- a/packages/compiler/src/core/binder.ts +++ b/packages/compiler/src/core/binder.ts @@ -21,6 +21,7 @@ import { ModelExpressionNode, ModelPropertyNode, ModelStatementNode, + ModifierFlags, MutableSymbolTable, NamespaceStatementNode, Node, @@ -208,6 +209,8 @@ export function createBinder(program: Program): Binder { parent: sourceFile, flags: NodeFlags.None, symbol: undefined!, + modifiers: [], + modifierFlags: ModifierFlags.None, }; const sym = createSymbol( jsNamespaceNode, @@ -375,7 +378,12 @@ export function createBinder(program: Program): Binder { } function bindModelStatement(node: ModelStatementNode) { - declareSymbol(node, SymbolFlags.Model | SymbolFlags.Declaration); + if (node.modifierFlags & ModifierFlags.Internal) debugger; + + const internal = + node.modifierFlags & ModifierFlags.Internal ? SymbolFlags.Internal : SymbolFlags.None; + + declareSymbol(node, SymbolFlags.Model | SymbolFlags.Declaration | internal); // Initialize locals for type parameters mutate(node).locals = new SymbolTable(); } @@ -393,7 +401,9 @@ export function createBinder(program: Program): Binder { } function bindScalarStatement(node: ScalarStatementNode) { - declareSymbol(node, SymbolFlags.Scalar | SymbolFlags.Declaration); + const internal = + node.modifierFlags & ModifierFlags.Internal ? SymbolFlags.Internal : SymbolFlags.None; + declareSymbol(node, SymbolFlags.Scalar | SymbolFlags.Declaration | internal); // Initialize locals for type parameters mutate(node).locals = new SymbolTable(); } @@ -403,26 +413,36 @@ export function createBinder(program: Program): Binder { } function bindInterfaceStatement(node: InterfaceStatementNode) { - declareSymbol(node, SymbolFlags.Interface | SymbolFlags.Declaration); + const internal = + node.modifierFlags & ModifierFlags.Internal ? SymbolFlags.Internal : SymbolFlags.None; + declareSymbol(node, SymbolFlags.Interface | SymbolFlags.Declaration | internal); mutate(node).locals = new SymbolTable(); } function bindUnionStatement(node: UnionStatementNode) { - declareSymbol(node, SymbolFlags.Union | SymbolFlags.Declaration); + const internal = + node.modifierFlags & ModifierFlags.Internal ? SymbolFlags.Internal : SymbolFlags.None; + declareSymbol(node, SymbolFlags.Union | SymbolFlags.Declaration | internal); mutate(node).locals = new SymbolTable(); } function bindAliasStatement(node: AliasStatementNode) { - declareSymbol(node, SymbolFlags.Alias | SymbolFlags.Declaration); + const internal = + node.modifierFlags & ModifierFlags.Internal ? SymbolFlags.Internal : SymbolFlags.None; + declareSymbol(node, SymbolFlags.Alias | SymbolFlags.Declaration | internal); // Initialize locals for type parameters mutate(node).locals = new SymbolTable(); } function bindConstStatement(node: ConstStatementNode) { - declareSymbol(node, SymbolFlags.Const | SymbolFlags.Declaration); + const internal = + node.modifierFlags & ModifierFlags.Internal ? SymbolFlags.Internal : SymbolFlags.None; + declareSymbol(node, SymbolFlags.Const | SymbolFlags.Declaration | internal); } function bindEnumStatement(node: EnumStatementNode) { - declareSymbol(node, SymbolFlags.Enum | SymbolFlags.Declaration); + const internal = + node.modifierFlags & ModifierFlags.Internal ? SymbolFlags.Internal : SymbolFlags.None; + declareSymbol(node, SymbolFlags.Enum | SymbolFlags.Declaration | internal); } function bindEnumMember(node: EnumMemberNode) { @@ -444,10 +464,14 @@ export function createBinder(program: Program): Binder { // locals are never shared. mutate(statement).locals = createSymbolTable(); mutate(existingBinding.declarations).push(statement); + + // TODO: report diagnostic if merging an internal and non-internal namespace } else { // Initialize locals for non-exported symbols mutate(statement).locals = createSymbolTable(); - declareSymbol(statement, SymbolFlags.Namespace | SymbolFlags.Declaration); + const internal = + statement.modifierFlags & ModifierFlags.Internal ? SymbolFlags.Internal : SymbolFlags.None; + declareSymbol(statement, SymbolFlags.Namespace | SymbolFlags.Declaration | internal); } currentFile.namespaces.push(statement); @@ -467,24 +491,34 @@ export function createBinder(program: Program): Binder { } function bindOperationStatement(statement: OperationStatementNode) { + const internal = + statement.modifierFlags & ModifierFlags.Internal ? SymbolFlags.Internal : SymbolFlags.None; if (scope.kind === SyntaxKind.InterfaceStatement) { declareMember( statement, - SymbolFlags.Operation | SymbolFlags.Member | SymbolFlags.Declaration, + SymbolFlags.Operation | SymbolFlags.Member | SymbolFlags.Declaration | internal, statement.id.sv, ); } else { - declareSymbol(statement, SymbolFlags.Operation | SymbolFlags.Declaration); + declareSymbol(statement, SymbolFlags.Operation | SymbolFlags.Declaration | internal); } mutate(statement).locals = createSymbolTable(); } function bindDecoratorDeclarationStatement(node: DecoratorDeclarationStatementNode) { - declareSymbol(node, SymbolFlags.Decorator | SymbolFlags.Declaration, `@${node.id.sv}`); + const internal = + node.modifierFlags & ModifierFlags.Internal ? SymbolFlags.Internal : SymbolFlags.None; + declareSymbol( + node, + SymbolFlags.Decorator | SymbolFlags.Declaration | internal, + `@${node.id.sv}`, + ); } function bindFunctionDeclarationStatement(node: FunctionDeclarationStatementNode) { - declareSymbol(node, SymbolFlags.Function | SymbolFlags.Declaration); + const internal = + node.modifierFlags & ModifierFlags.Internal ? SymbolFlags.Internal : SymbolFlags.None; + declareSymbol(node, SymbolFlags.Function | SymbolFlags.Declaration | internal); } function bindFunctionParameter(node: FunctionParameterNode) { @@ -504,7 +538,11 @@ export function createBinder(program: Program): Binder { * @param name Optional symbol name, default to the node id. * @returns Created Symbol */ - function declareSymbol(node: Declaration, flags: SymbolFlags, name?: string) { + function declareSymbol( + node: Declaration | TemplateParameterDeclarationNode, + flags: SymbolFlags, + name?: string, + ) { compilerAssert(flags & SymbolFlags.Declaration, `Expected declaration symbol: ${name}`, node); switch (scope.kind) { case SyntaxKind.NamespaceStatement: @@ -527,7 +565,11 @@ export function createBinder(program: Program): Binder { return symbol; } - function declareNamespaceMember(node: Declaration, flags: SymbolFlags, name?: string) { + function declareNamespaceMember( + node: Declaration | TemplateParameterDeclarationNode, + flags: SymbolFlags, + name?: string, + ) { if ( flags & SymbolFlags.Namespace && mergeNamespaceDeclarations(node as NamespaceStatementNode, scope) @@ -541,7 +583,11 @@ export function createBinder(program: Program): Binder { return symbol; } - function declareScriptMember(node: Declaration, flags: SymbolFlags, name?: string) { + function declareScriptMember( + node: Declaration | TemplateParameterDeclarationNode, + flags: SymbolFlags, + name?: string, + ) { const effectiveScope = fileNamespace ?? scope; if ( flags & SymbolFlags.Namespace && diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 3802783dc4f..d68129f3678 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -12,6 +12,7 @@ import { import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; import { compilerAssert, ignoreDiagnostics } from "./diagnostics.js"; import { validateInheritanceDiscriminatedUnions } from "./helpers/discriminator-utils.js"; +import { getLocationContext } from "./helpers/location-context.js"; import { explainStringTemplateNotSerializable } from "./helpers/string-template-utils.js"; import { typeReferenceToString } from "./helpers/syntax-utils.js"; import { getEntityName, getTypeName } from "./helpers/type-name-utils.js"; @@ -80,6 +81,7 @@ import { JsNamespaceDeclarationNode, LiteralNode, LiteralType, + LocationContext, MemberContainerNode, MemberContainerType, MemberExpressionNode, @@ -3022,6 +3024,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, mapper: TypeMapper | undefined, options?: Partial | boolean, + locationContext?: LocationContext, ): Sym | undefined { const resolvedOptions: SymbolResolutionOptions = typeof options === "boolean" @@ -3030,7 +3033,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker if (mapper === undefined && resolvedOptions.checkTemplateTypes && referenceSymCache.has(node)) { return referenceSymCache.get(node); } - const sym = resolveTypeReferenceSymInternal(node, mapper, resolvedOptions); + + locationContext ??= getLocationContext(program, node); + + const sym = resolveTypeReferenceSymInternal(node, mapper, resolvedOptions, locationContext); if (resolvedOptions.checkTemplateTypes) { referenceSymCache.set(node, sym); } @@ -3041,6 +3047,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, mapper: TypeMapper | undefined, options: SymbolResolutionOptions, + locationContext: LocationContext, ): Sym | undefined { if (hasParseError(node)) { // Don't report synthetic identifiers used for parser error recovery. @@ -3048,7 +3055,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return undefined; } if (node.kind === SyntaxKind.TypeReference) { - return resolveTypeReferenceSym(node.target, mapper, options); + return resolveTypeReferenceSym(node.target, mapper, options, locationContext); } else if (node.kind === SyntaxKind.Identifier) { const links = resolver.getNodeLinks(node); if (mapper === undefined && links.resolutionResult) { @@ -3070,13 +3077,21 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } } - const sym = links.resolvedSymbol; - return sym?.symbolSource ?? sym; + const sym = links.resolvedSymbol?.symbolSource ?? links.resolvedSymbol; + + checkSymbolAccess(locationContext, node, sym); + + return sym; } else if (node.kind === SyntaxKind.MemberExpression) { - let base = resolveTypeReferenceSym(node.base, mapper, { - ...options, - resolveDecorators: false, // when resolving decorator the base cannot also be one - }); + let base = resolveTypeReferenceSym( + node.base, + mapper, + { + ...options, + resolveDecorators: false, // when resolving decorator the base cannot also be one + }, + locationContext, + ); if (!base) { return undefined; @@ -3103,12 +3118,62 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } base = aliasedSym; } - return resolveMemberInContainer(base, node, options); + const sym = resolveMemberInContainer(base, node, options); + + checkSymbolAccess(locationContext, node, sym); + + return sym; } compilerAssert(false, `Unknown type reference kind "${SyntaxKind[(node as any).kind]}"`, node); } + function checkSymbolAccess(sourceLocation: LocationContext, node: Node, symbol: Sym | undefined) { + if (!symbol) return; + + if (symbol.flags & SymbolFlags.Internal) debugger; + + const isInternalDeclaration = + (symbol.flags & (SymbolFlags.Internal | SymbolFlags.Declaration)) === + (SymbolFlags.Internal | SymbolFlags.Declaration); + + if (isInternalDeclaration) { + // The source location can access internal declaration symbols if: + // 1. The source location is synthetic. + // 2. The source location is in the compiler standard library. + // 3. SOME declaration of the target symbol meets the following: + // 1. The source location is in the user project, and the symbol is also declared in the user project. + // 2. The source location is in a library, and the symbol is also in the same library. + + if (sourceLocation.type === "synthetic" || sourceLocation.type === "compiler") { + return; + } + + const isDeclaredInCompatibleLocation = symbol.declarations.some((decl) => { + const declLocation = getLocationContext(program, decl); + + if (declLocation.type !== sourceLocation.type) return false; + + // Both are project + if (declLocation.type === "project") return true; + + // Both are library, use reference equality to check if they are the same library. + return declLocation === sourceLocation; + }); + + if (isDeclaredInCompatibleLocation) return; + + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-ref", + messageId: "internal", + format: { id: symbol.name }, + target: node, + }), + ); + } + } + function reportAmbiguousIdentifier(node: IdentifierNode, symbols: Sym[]) { const duplicateNames = symbols.map((s) => getFullyQualifiedSymbolName(s, { useGlobalPrefixAtTopLevel: true }), diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 5c42d275d59..d3f2024aee5 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -325,6 +325,7 @@ const diagnostics = { member: paramMessage`${"kind"} doesn't have member ${"id"}`, metaProperty: paramMessage`${"kind"} doesn't have meta property ${"id"}`, node: paramMessage`Cannot resolve '${"id"}' in node ${"nodeName"} since it has no members. Did you mean to use "::" instead of "."?`, + internal: paramMessage`Symbol '${"id"}' is internal and can only be accessed from within its declaring package.`, }, }, "duplicate-property": { diff --git a/packages/compiler/src/core/modifiers.ts b/packages/compiler/src/core/modifiers.ts new file mode 100644 index 00000000000..1eb6e1996be --- /dev/null +++ b/packages/compiler/src/core/modifiers.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT License. + +import { Program } from "./program.js"; +import { Declaration, ModifierFlags, SyntaxKind } from "./types.js"; + +/** + * The compatibility of modifiers for a given declaration node type. + */ +interface ModifierCompatibility { + /** A set of modifier flags that are allowed on the node type. */ + readonly allowed: ModifierFlags; + /** A set of modifier flags that are _required_ on the node type. */ + readonly required: ModifierFlags; +} + +/** + * The default compatibility for all declaration syntax nodes. + * + * By default, only the `internal` modifier is allowed on all declaration syntax nodes. + * No modifiers are required by default. + */ +const DEFAULT_COMPATIBILITY: ModifierCompatibility = { + allowed: ModifierFlags.Internal, + required: ModifierFlags.None, +}; + +const SYNTAX_MODIFIERS: Readonly> = { + [SyntaxKind.NamespaceStatement]: DEFAULT_COMPATIBILITY, + [SyntaxKind.OperationStatement]: DEFAULT_COMPATIBILITY, + [SyntaxKind.ModelStatement]: DEFAULT_COMPATIBILITY, + [SyntaxKind.ScalarStatement]: DEFAULT_COMPATIBILITY, + [SyntaxKind.InterfaceStatement]: DEFAULT_COMPATIBILITY, + [SyntaxKind.UnionStatement]: DEFAULT_COMPATIBILITY, + [SyntaxKind.EnumStatement]: DEFAULT_COMPATIBILITY, + [SyntaxKind.AliasStatement]: DEFAULT_COMPATIBILITY, + [SyntaxKind.FunctionDeclarationStatement]: DEFAULT_COMPATIBILITY, + [SyntaxKind.ConstStatement]: DEFAULT_COMPATIBILITY, + [SyntaxKind.DecoratorDeclarationStatement]: { + allowed: ModifierFlags.All, + required: ModifierFlags.Extern, + }, +}; + +/** + * Checks the modifiers on a declaration node against the allowed and required modifiers. + * + * This will report diagnostics in the given program if there are any invalid or missing required modifiers. + * + * @param program - The current program (used to report diagnostics). + * @param node - The declaration node to check. + * @returns `true` if the modifiers are valid, `false` otherwise. + */ +export function checkModifiers(program: Program, node: Declaration): boolean { + const compatibility = SYNTAX_MODIFIERS[node.kind]; + + let isValid = true; + + if (node.modifierFlags & ~compatibility.allowed) { + // There is at least one modifier used that is not allowed on this syntax node. + isValid = false; + + // TODO: report diagnostic "Modifier 'X' is not allowed on Y declarations." + } + + if ((node.modifierFlags & compatibility.required) !== compatibility.required) { + // There is at least one required modifier missing from this syntax node. + isValid = false; + + // TODO: report diagnostic "Modifier 'X' is required for Y declarations." + } + + return isValid; +} diff --git a/packages/compiler/src/core/name-resolver.ts b/packages/compiler/src/core/name-resolver.ts index 4af6b59be47..6a1dcc50fb1 100644 --- a/packages/compiler/src/core/name-resolver.ts +++ b/packages/compiler/src/core/name-resolver.ts @@ -75,6 +75,7 @@ import { ModelExpressionNode, ModelPropertyNode, ModelStatementNode, + ModifierFlags, NamespaceStatementNode, Node, NodeFlags, @@ -1211,6 +1212,8 @@ export function createResolver(program: Program): NameResolver { symbol: undefined!, locals: createSymbolTable(), flags: NodeFlags.Synthetic, + modifiers: [], + modifierFlags: ModifierFlags.None, }; return nsNode; diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 62b1c3d7b23..0664ba9874d 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -27,6 +27,7 @@ import { CallExpressionNode, Comment, ConstStatementNode, + Declaration, DeclarationNode, DecoratorDeclarationStatementNode, DecoratorExpressionNode, @@ -57,6 +58,7 @@ import { IdentifierNode, ImportStatementNode, InterfaceStatementNode, + InternalKeywordNode, InvalidStatementNode, LineComment, MemberExpressionNode, @@ -435,35 +437,32 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa reportInvalidDecorators(decorators, "import statement"); item = parseImportStatement(); break; - case Token.ModelKeyword: - item = parseModelStatement(pos, decorators); - break; - case Token.ScalarKeyword: - item = parseScalarStatement(pos, decorators); - break; - case Token.NamespaceKeyword: - item = parseNamespaceStatement(pos, decorators, docs, directives); - break; - case Token.InterfaceKeyword: - item = parseInterfaceStatement(pos, decorators); - break; - case Token.UnionKeyword: - item = parseUnionStatement(pos, decorators); - break; - case Token.OpKeyword: - item = parseOperationStatement(pos, decorators); - break; - case Token.EnumKeyword: - item = parseEnumStatement(pos, decorators); - break; - case Token.AliasKeyword: - reportInvalidDecorators(decorators, "alias statement"); - item = parseAliasStatement(pos); - break; - case Token.ConstKeyword: - reportInvalidDecorators(decorators, "const statement"); - item = parseConstStatement(pos); - break; + // case Token.ModelKeyword: + // item = parseModelStatement(pos, decorators); + // break; + // case Token.ScalarKeyword: + // item = parseScalarStatement(pos, decorators); + // break; + // case Token.InterfaceKeyword: + // item = parseInterfaceStatement(pos, decorators); + // break; + // case Token.UnionKeyword: + // item = parseUnionStatement(pos, decorators); + // break; + // case Token.OpKeyword: + // item = parseOperationStatement(pos, decorators); + // break; + // case Token.EnumKeyword: + // item = parseEnumStatement(pos, decorators); + // break; + // case Token.AliasKeyword: + // reportInvalidDecorators(decorators, "alias statement"); + // item = parseAliasStatement(pos); + // break; + // case Token.ConstKeyword: + // reportInvalidDecorators(decorators, "const statement"); + // item = parseConstStatement(pos); + // break; case Token.UsingKeyword: reportInvalidDecorators(decorators, "using statement"); item = parseUsingStatement(pos); @@ -473,10 +472,20 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa item = parseEmptyStatement(pos); break; // Start of declaration with modifiers + case Token.NamespaceKeyword: + case Token.ModelKeyword: + case Token.ScalarKeyword: + case Token.InterfaceKeyword: + case Token.UnionKeyword: + case Token.OpKeyword: + case Token.EnumKeyword: + case Token.AliasKeyword: + case Token.ConstKeyword: case Token.ExternKeyword: + case Token.InternalKeyword: case Token.FnKeyword: case Token.DecKeyword: - item = parseDeclaration(pos); + item = parseDeclaration(pos, decorators, docs, directives); break; default: item = parseInvalidStatement(pos, decorators); @@ -530,48 +539,24 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa item = parseImportStatement(); error({ code: "import-first", messageId: "topLevel", target: item }); break; - case Token.ModelKeyword: - item = parseModelStatement(pos, decorators); + case Token.UsingKeyword: + reportInvalidDecorators(decorators, "using statement"); + item = parseUsingStatement(pos); break; + case Token.ModelKeyword: case Token.ScalarKeyword: - item = parseScalarStatement(pos, decorators); - break; case Token.NamespaceKeyword: - const ns = parseNamespaceStatement(pos, decorators, docs, directives); - - if (isBlocklessNamespace(ns)) { - error({ code: "blockless-namespace-first", messageId: "topLevel", target: ns }); - } - item = ns; - break; case Token.InterfaceKeyword: - item = parseInterfaceStatement(pos, decorators); - break; case Token.UnionKeyword: - item = parseUnionStatement(pos, decorators); - break; case Token.OpKeyword: - item = parseOperationStatement(pos, decorators); - break; case Token.EnumKeyword: - item = parseEnumStatement(pos, decorators); - break; case Token.AliasKeyword: - reportInvalidDecorators(decorators, "alias statement"); - item = parseAliasStatement(pos); - break; case Token.ConstKeyword: - reportInvalidDecorators(decorators, "const statement"); - item = parseConstStatement(pos); - break; - case Token.UsingKeyword: - reportInvalidDecorators(decorators, "using statement"); - item = parseUsingStatement(pos); - break; case Token.ExternKeyword: + case Token.InternalKeyword: case Token.FnKeyword: case Token.DecKeyword: - item = parseDeclaration(pos); + item = parseDeclaration(pos, decorators, docs, directives); break; case Token.EndOfFile: parseExpected(Token.CloseBrace); @@ -584,6 +569,11 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa item = parseInvalidStatement(pos, decorators); break; } + + if (isBlocklessNamespace(item)) { + error({ code: "blockless-namespace-first", messageId: "topLevel", target: item }); + } + mutate(item).directives = directives; if (tok !== Token.NamespaceKeyword) { mutate(item).docs = docs; @@ -617,6 +607,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseNamespaceStatement( pos: number, decorators: DecoratorExpressionNode[], + modifiers: Modifier[], docs: DocNode[], directives: DirectiveExpressionNode[], ): NamespaceStatementNode { @@ -645,6 +636,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa locals: undefined!, statements, directives: directives, + modifiers, + modifierFlags: modifiersToFlags(modifiers), ...finishNode(pos), }; @@ -656,6 +649,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa id: nsSegments[i], statements: outerNs, locals: undefined!, + modifiers: [], + modifierFlags: ModifierFlags.None, ...finishNode(pos), }; } @@ -666,6 +661,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseInterfaceStatement( pos: number, decorators: DecoratorExpressionNode[], + modifiers: Modifier[], ): InterfaceStatementNode { parseExpected(Token.InterfaceKeyword); const id = parseIdentifier(); @@ -683,7 +679,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const { items: operations, range: bodyRange } = parseList( ListKind.InterfaceMembers, - (pos, decorators) => parseOperationStatement(pos, decorators, true), + (pos, decorators) => + parseOperationStatement(pos, decorators, /* modifiers */ undefined, true), ); return { @@ -695,6 +692,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa bodyRange, extends: extendList.items, decorators, + modifiers, + modifierFlags: modifiersToFlags(modifiers), ...finishNode(pos), }; } @@ -719,6 +718,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseUnionStatement( pos: number, decorators: DecoratorExpressionNode[], + modifiers: Modifier[], ): UnionStatementNode { parseExpected(Token.UnionKeyword); const id = parseIdentifier(); @@ -733,6 +733,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa templateParameters, templateParametersRange, decorators, + modifiers, + modifierFlags: modifiersToFlags(modifiers), options, ...finishNode(pos), }; @@ -820,11 +822,27 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseOperationStatement( pos: number, decorators: DecoratorExpressionNode[], + modifiers: Modifier[], + inInterface?: undefined | false, + ): OperationStatementNode; + function parseOperationStatement( + pos: number, + decorators: DecoratorExpressionNode[], + modifiers: undefined, + inInterface: true, + ): OperationStatementNode; + function parseOperationStatement( + pos: number, + decorators: DecoratorExpressionNode[], + _modifiers: Modifier[] | undefined, inInterface?: boolean, ): OperationStatementNode { + let modifiers: Modifier[]; if (inInterface) { + modifiers = parseModifiers(); parseOptional(Token.OpKeyword); } else { + modifiers = _modifiers as Modifier[]; parseExpected(Token.OpKeyword); } @@ -872,6 +890,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa templateParametersRange, signature, decorators, + modifiers, + modifierFlags: modifiersToFlags(modifiers), ...finishNode(pos), }; } @@ -894,6 +914,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseModelStatement( pos: number, decorators: DecoratorExpressionNode[], + modifiers: Modifier[], ): ModelStatementNode { parseExpected(Token.ModelKeyword); const id = parseIdentifier(); @@ -929,6 +950,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa decorators, properties: propDetail.items, bodyRange: propDetail.range, + modifiers, + modifierFlags: modifiersToFlags(modifiers), ...finishNode(pos), }; } @@ -1096,6 +1119,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseScalarStatement( pos: number, decorators: DecoratorExpressionNode[], + modifiers: Modifier[], ): ScalarStatementNode { parseExpected(Token.ScalarKeyword); const id = parseIdentifier(); @@ -1114,6 +1138,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa members, bodyRange, decorators, + modifiers, + modifierFlags: modifiersToFlags(modifiers), ...finishNode(pos), }; } @@ -1154,6 +1180,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseEnumStatement( pos: number, decorators: DecoratorExpressionNode[], + modifiers: Modifier[], ): EnumStatementNode { parseExpected(Token.EnumKeyword); const id = parseIdentifier(); @@ -1162,6 +1189,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa kind: SyntaxKind.EnumStatement, id, decorators, + modifiers, + modifierFlags: modifiersToFlags(modifiers), members, ...finishNode(pos), }; @@ -1222,7 +1251,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } - function parseAliasStatement(pos: number): AliasStatementNode { + function parseAliasStatement(pos: number, modifiers: Modifier[]): AliasStatementNode { parseExpected(Token.AliasKeyword); const id = parseIdentifier(); const { items: templateParameters, range: templateParametersRange } = @@ -1236,11 +1265,13 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa templateParameters, templateParametersRange, value, + modifiers, + modifierFlags: modifiersToFlags(modifiers), ...finishNode(pos), }; } - function parseConstStatement(pos: number): ConstStatementNode { + function parseConstStatement(pos: number, modifiers: Modifier[]): ConstStatementNode { parseExpected(Token.ConstKeyword); const id = parseIdentifier(); const type = parseOptionalTypeAnnotation(); @@ -1252,6 +1283,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa id, value, type, + modifiers, + modifierFlags: modifiersToFlags(modifiers), ...finishNode(pos), }; } @@ -1716,6 +1749,15 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } + function parseInternalKeyword(): InternalKeywordNode { + const pos = tokenPos(); + parseExpected(Token.InternalKeyword); + return { + kind: SyntaxKind.InternalKeyword, + ...finishNode(pos), + }; + } + function parseVoidKeyword(): VoidKeywordNode { const pos = tokenPos(); parseExpected(Token.VoidKeyword); @@ -1985,9 +2027,32 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseDeclaration( pos: number, - ): DecoratorDeclarationStatementNode | FunctionDeclarationStatementNode | InvalidStatementNode { + decorators: DecoratorExpressionNode[], + docs: DocNode[], + directives: DirectiveExpressionNode[], + ): Declaration | InvalidStatementNode { const modifiers = parseModifiers(); switch (token()) { + case Token.ModelKeyword: + return parseModelStatement(pos, decorators, modifiers); + case Token.ScalarKeyword: + return parseScalarStatement(pos, decorators, modifiers); + case Token.NamespaceKeyword: + return parseNamespaceStatement(pos, decorators, modifiers, docs, directives); + case Token.InterfaceKeyword: + return parseInterfaceStatement(pos, decorators, modifiers); + case Token.UnionKeyword: + return parseUnionStatement(pos, decorators, modifiers); + case Token.OpKeyword: + return parseOperationStatement(pos, decorators, modifiers); + case Token.EnumKeyword: + return parseEnumStatement(pos, decorators, modifiers); + case Token.AliasKeyword: + reportInvalidDecorators(decorators, "alias statement"); + return parseAliasStatement(pos, modifiers); + case Token.ConstKeyword: + reportInvalidDecorators(decorators, "const statement"); + return parseConstStatement(pos, modifiers); case Token.DecKeyword: return parseDecoratorDeclarationStatement(pos, modifiers); case Token.FnKeyword: @@ -2009,6 +2074,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa switch (token()) { case Token.ExternKeyword: return parseExternKeyword(); + case Token.InternalKeyword: + return parseInternalKeyword(); default: return undefined; } @@ -2127,6 +2194,9 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa case SyntaxKind.ExternKeyword: flags |= ModifierFlags.Extern; break; + case SyntaxKind.InternalKeyword: + flags |= ModifierFlags.Internal; + break; } } return flags; @@ -3076,6 +3146,7 @@ export function visitChildren(node: Node, cb: NodeCallback): T | undefined case SyntaxKind.VoidKeyword: case SyntaxKind.NeverKeyword: case SyntaxKind.ExternKeyword: + case SyntaxKind.InternalKeyword: case SyntaxKind.UnknownKeyword: case SyntaxKind.JsSourceFile: case SyntaxKind.JsNamespaceDeclaration: diff --git a/packages/compiler/src/core/source-loader.ts b/packages/compiler/src/core/source-loader.ts index 573af0a5363..b3c70d3d22c 100644 --- a/packages/compiler/src/core/source-loader.ts +++ b/packages/compiler/src/core/source-loader.ts @@ -16,6 +16,7 @@ import { getDirectoryPath } from "./path-utils.js"; import { createSourceFile } from "./source-file.js"; import { DiagnosticTarget, + ModifierFlags, ModuleLibraryMetadata, NodeFlags, NoTarget, @@ -401,6 +402,8 @@ export async function loadJsFile( pos: 0, end: 0, flags: NodeFlags.None, + modifiers: [], + modifierFlags: ModifierFlags.None, }; return [node, diagnostics]; } diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 4cb2b995198..5541bb00316 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -935,6 +935,11 @@ export const enum SymbolFlags { */ LateBound = 1 << 22, + /** + * An internal symbol that can only be referenced from a source file in the same package. + */ + Internal = 1 << 23, + ExportContainer = Namespace | SourceFile, /** * Symbols whose members will be late bound (and stored on the type) @@ -1039,6 +1044,7 @@ export enum SyntaxKind { ConstStatement, CallExpression, ScalarConstructor, + InternalKeyword, } export const enum NodeFlags { @@ -1219,8 +1225,9 @@ export interface ParseOptions { readonly docs?: boolean; } -export interface TypeSpecScriptNode extends DeclarationNode, BaseNode { +export interface TypeSpecScriptNode extends BaseNode { readonly kind: SyntaxKind.TypeSpecScript; + readonly id: IdentifierNode; readonly statements: readonly Statement[]; readonly file: SourceFile; readonly inScopeNamespaces: readonly NamespaceStatementNode[]; // namespaces that declarations in this file belong to @@ -1253,22 +1260,23 @@ export type Statement = | InvalidStatementNode; export interface DeclarationNode { + /** + * Identifier that this node declares. + */ readonly id: IdentifierNode; + + /** + * Modifier nodes applied to this declaration. + */ + readonly modifiers: Modifier[]; + + /** + * Combined modifier flags for this declaration. + */ + readonly modifierFlags: ModifierFlags; } -export type Declaration = - | ModelStatementNode - | ScalarStatementNode - | InterfaceStatementNode - | UnionStatementNode - | NamespaceStatementNode - | OperationStatementNode - | TemplateParameterDeclarationNode - | EnumStatementNode - | AliasStatementNode - | ConstStatementNode - | DecoratorDeclarationStatementNode - | FunctionDeclarationStatementNode; +export type Declaration = Extract; export type ScopeNode = | NamespaceStatementNode @@ -1595,6 +1603,10 @@ export interface ExternKeywordNode extends BaseNode { readonly kind: SyntaxKind.ExternKeyword; } +export interface InternalKeywordNode extends BaseNode { + readonly kind: SyntaxKind.InternalKeyword; +} + export interface VoidKeywordNode extends BaseNode { readonly kind: SyntaxKind.VoidKeyword; } @@ -1639,19 +1651,23 @@ export interface TemplateArgumentNode extends BaseNode { readonly argument: Expression; } -export interface TemplateParameterDeclarationNode extends DeclarationNode, BaseNode { +export interface TemplateParameterDeclarationNode extends BaseNode { readonly kind: SyntaxKind.TemplateParameterDeclaration; readonly constraint?: Expression; readonly default?: Expression; readonly parent?: TemplateableNode; + readonly id: IdentifierNode; } export const enum ModifierFlags { None, Extern = 1 << 1, + Internal = 1 << 2, + + All = Extern | Internal, } -export type Modifier = ExternKeywordNode; +export type Modifier = ExternKeywordNode | InternalKeywordNode; /** * Represent a decorator declaration @@ -1662,8 +1678,6 @@ export type Modifier = ExternKeywordNode; */ export interface DecoratorDeclarationStatementNode extends BaseNode, DeclarationNode { readonly kind: SyntaxKind.DecoratorDeclarationStatement; - readonly modifiers: readonly Modifier[]; - readonly modifierFlags: ModifierFlags; /** * Decorator target. First parameter. */ @@ -1701,8 +1715,6 @@ export interface FunctionParameterNode extends BaseNode { */ export interface FunctionDeclarationStatementNode extends BaseNode, DeclarationNode { readonly kind: SyntaxKind.FunctionDeclarationStatement; - readonly modifiers: readonly Modifier[]; - readonly modifierFlags: ModifierFlags; readonly parameters: FunctionParameterNode[]; readonly returnType?: Expression; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index a06100528ba..8c9db65a086 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -249,6 +249,8 @@ export function printNode( ); case SyntaxKind.ExternKeyword: return "extern"; + case SyntaxKind.InternalKeyword: + return "internal"; case SyntaxKind.VoidKeyword: return "void"; case SyntaxKind.NeverKeyword: diff --git a/packages/compiler/src/server/tmlanguage.ts b/packages/compiler/src/server/tmlanguage.ts index 283bf35e171..b681ea4aea1 100644 --- a/packages/compiler/src/server/tmlanguage.ts +++ b/packages/compiler/src/server/tmlanguage.ts @@ -65,7 +65,7 @@ const simpleIdentifier = `\\b${identifierStart}${identifierContinue}*\\b`; const identifier = `${simpleIdentifier}|${escapedIdentifier}`; const qualifiedIdentifier = `\\b${identifierStart}(?:${identifierContinue}|\\.${identifierStart})*\\b`; const stringPattern = '\\"(?:[^\\"\\\\]|\\\\.)*\\"'; -const modifierKeyword = `\\b(?:extern)\\b`; +const modifierKeyword = `\\b(?:extern|internal)\\b`; const statementKeyword = `\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b`; const universalEnd = `(?=,|;|@|\\)|\\}|${modifierKeyword}|${statementKeyword})`; const universalEndExceptComma = `(?=;|@|\\)|\\}|${modifierKeyword}|${statementKeyword})`; diff --git a/packages/compiler/test/binder.test.ts b/packages/compiler/test/binder.test.ts index 012c32162fa..261bef6c83c 100644 --- a/packages/compiler/test/binder.test.ts +++ b/packages/compiler/test/binder.test.ts @@ -11,6 +11,7 @@ import { InterfaceStatementNode, JsSourceFileNode, ModelStatementNode, + ModifierFlags, NodeFlags, Sym, SymbolFlags, @@ -531,5 +532,7 @@ function createJsSourceFile(exports: any): JsSourceFileNode { pos: 0, end: 0, flags: NodeFlags.None, + modifiers: [], + modifierFlags: ModifierFlags.None, }; } diff --git a/packages/compiler/test/name-resolver.test.ts b/packages/compiler/test/name-resolver.test.ts index 788d1be79e3..0aa8797d756 100644 --- a/packages/compiler/test/name-resolver.test.ts +++ b/packages/compiler/test/name-resolver.test.ts @@ -11,6 +11,7 @@ import { IdentifierNode, JsSourceFileNode, MemberExpressionNode, + ModifierFlags, Node, NodeFlags, ResolutionResult, @@ -1520,5 +1521,7 @@ function createJsSourceFile(exports: any): JsSourceFileNode { pos: 0, end: 0, flags: NodeFlags.None, + modifiers: [], + modifierFlags: ModifierFlags.None, }; } From f731fc8f6ebce2d6c71cb7ce49fea27bdf9e202f Mon Sep 17 00:00:00 2001 From: Will Temple Date: Tue, 28 Oct 2025 10:42:25 -0400 Subject: [PATCH 02/14] More checking. --- packages/compiler/src/core/checker.ts | 15 +++ packages/compiler/src/core/messages.ts | 8 ++ packages/compiler/src/core/modifiers.ts | 118 +++++++++++++++++++++++- packages/compiler/src/core/parser.ts | 16 +--- 4 files changed, 138 insertions(+), 19 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index d68129f3678..7504e241af5 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -18,6 +18,7 @@ import { typeReferenceToString } from "./helpers/syntax-utils.js"; import { getEntityName, getTypeName } from "./helpers/type-name-utils.js"; import { marshallTypeForJS } from "./js-marshaller.js"; import { createDiagnostic } from "./messages.js"; +import { checkModifiers } from "./modifiers.js"; import { NameResolver } from "./name-resolver.js"; import { Numeric } from "./numeric.js"; import { @@ -1867,6 +1868,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker node: DecoratorDeclarationStatementNode, mapper: TypeMapper | undefined, ): Decorator { + checkModifiers(program, node); const symbol = getMergedSymbol(node.symbol); const links = getSymbolLinks(symbol); if (links.declaredType && mapper === undefined) { @@ -1910,6 +1912,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker node: FunctionDeclarationStatementNode, mapper: TypeMapper | undefined, ) { + checkModifiers(program, node); reportCheckerDiagnostic(createDiagnostic({ code: "function-unsupported", target: node })); return errorType; } @@ -2123,6 +2126,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } if (node.kind === SyntaxKind.NamespaceStatement) { + checkModifiers(program, node); if (isArray(node.statements)) { node.statements.forEach((x) => checkNode(x)); } else if (node.statements) { @@ -2260,6 +2264,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker mapper: TypeMapper | undefined, parentInterface?: Interface, ): Operation { + checkModifiers(program, node); const inInterface = node.parent?.kind === SyntaxKind.InterfaceStatement; const symbol = inInterface ? getSymbolForMember(node) : node.symbol; const links = symbol && getSymbolLinks(symbol); @@ -3594,6 +3599,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkModelStatement(node: ModelStatementNode, mapper: TypeMapper | undefined): Model { + checkModifiers(program, node); const links = getSymbolLinks(node.symbol); if (links.declaredType && mapper === undefined) { @@ -5279,6 +5285,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkScalar(node: ScalarStatementNode, mapper: TypeMapper | undefined): Scalar { + checkModifiers(program, node); const links = getSymbolLinks(node.symbol); if (links.declaredType && mapper === undefined) { @@ -5426,6 +5433,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker node: AliasStatementNode, mapper: TypeMapper | undefined, ): Type | IndeterminateEntity { + checkModifiers(program, node); + const links = getSymbolLinks(node.symbol); if (links.declaredType && mapper === undefined) { @@ -5466,6 +5475,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkConst(node: ConstStatementNode): Value | null { + checkModifiers(program, node); + const links = getSymbolLinks(node.symbol); if (links.value !== undefined) { return links.value; @@ -5515,6 +5526,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkEnum(node: EnumStatementNode, mapper: TypeMapper | undefined): Type { + checkModifiers(program, node); + const links = getSymbolLinks(node.symbol); if (!links.type) { const enumType: Enum = (links.type = createType({ @@ -5569,6 +5582,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkInterface(node: InterfaceStatementNode, mapper: TypeMapper | undefined): Interface { + checkModifiers(program, node); const links = getSymbolLinks(node.symbol); if (links.declaredType && mapper === undefined) { @@ -5671,6 +5685,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkUnion(node: UnionStatementNode, mapper: TypeMapper | undefined) { + checkModifiers(program, node); const links = getSymbolLinks(node.symbol); if (links.declaredType && mapper === undefined) { diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index d3f2024aee5..e4de9b925ea 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -540,6 +540,14 @@ const diagnostics = { default: "A function declaration must be prefixed with the 'extern' modifier.", }, }, + "invalid-modifier": { + severity: "error", + messages: { + default: paramMessage`Modifier '${"modifier"}' is invalid.`, + "missing-required": paramMessage`Declaration of type '${"nodeKind"}' is missing required modifier '${"modifier"}'.`, + "not-allowed": paramMessage`Modifier '${"modifier"}' cannot be used on declarations of type '${"nodeKind"}'.`, + }, + }, "function-unsupported": { severity: "error", messages: { diff --git a/packages/compiler/src/core/modifiers.ts b/packages/compiler/src/core/modifiers.ts index 1eb6e1996be..1625899d8b3 100644 --- a/packages/compiler/src/core/modifiers.ts +++ b/packages/compiler/src/core/modifiers.ts @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation // Licensed under the MIT License. +import { compilerAssert } from "./diagnostics.js"; +import { createDiagnostic } from "./messages.js"; import { Program } from "./program.js"; -import { Declaration, ModifierFlags, SyntaxKind } from "./types.js"; +import { Declaration, Modifier, ModifierFlags, SyntaxKind } from "./types.js"; /** * The compatibility of modifiers for a given declaration node type. @@ -56,19 +58,127 @@ export function checkModifiers(program: Program, node: Declaration): boolean { let isValid = true; - if (node.modifierFlags & ~compatibility.allowed) { + const invalidModifiers = node.modifierFlags & ~compatibility.allowed; + + if (invalidModifiers) { // There is at least one modifier used that is not allowed on this syntax node. isValid = false; - // TODO: report diagnostic "Modifier 'X' is not allowed on Y declarations." + const invalidModifierList = filterModifiersByFlags(node.modifiers, invalidModifiers); + + for (const modifier of invalidModifierList) { + const modifierText = getTextForModifier(modifier); + program.reportDiagnostic( + createDiagnostic({ + code: "invalid-modifier", + messageId: "not-allowed", + format: { modifier: modifierText, nodeKind: getDeclarationKindText(node.kind) }, + target: modifier, + }), + ); + } } - if ((node.modifierFlags & compatibility.required) !== compatibility.required) { + const missingRequiredModifiers = compatibility.required & ~node.modifierFlags; + + if (missingRequiredModifiers) { // There is at least one required modifier missing from this syntax node. isValid = false; // TODO: report diagnostic "Modifier 'X' is required for Y declarations." + for (const missing of getNamesOfModifierFlags(missingRequiredModifiers)) { + program.reportDiagnostic( + createDiagnostic({ + code: "invalid-modifier", + messageId: "missing-required", + format: { modifier: missing, nodeKind: getDeclarationKindText(node.kind) }, + target: node, + }), + ); + } } return isValid; } + +function filterModifiersByFlags(modifiers: Modifier[], flags: ModifierFlags): Modifier[] { + const result = []; + + for (const modifier of modifiers) { + if (modifierToFlag(modifier) & flags) { + result.push(modifier); + } + } + + return result; +} + +export function modifiersToFlags(modifiers: Modifier[]): ModifierFlags { + let flags = ModifierFlags.None; + for (const modifier of modifiers) { + flags |= modifierToFlag(modifier); + } + return flags; +} + +function modifierToFlag(modifier: Modifier): ModifierFlags { + switch (modifier.kind) { + case SyntaxKind.ExternKeyword: + return ModifierFlags.Extern; + case SyntaxKind.InternalKeyword: + return ModifierFlags.Internal; + default: + compilerAssert(false, `Unknown modifier kind: ${(modifier as Modifier).kind}`); + } +} + +function getTextForModifier(modifier: Modifier): string { + switch (modifier.kind) { + case SyntaxKind.ExternKeyword: + return "extern"; + case SyntaxKind.InternalKeyword: + return "internal"; + default: + compilerAssert(false, `Unknown modifier kind: ${(modifier as Modifier).kind}`); + } +} + +function getNamesOfModifierFlags(flags: ModifierFlags): string[] { + const names: string[] = []; + if (flags & ModifierFlags.Extern) { + names.push("extern"); + } + if (flags & ModifierFlags.Internal) { + names.push("internal"); + } + return names; +} + +function getDeclarationKindText(nodeKind: Declaration["kind"]): string { + switch (nodeKind) { + case SyntaxKind.NamespaceStatement: + return "namespace"; + case SyntaxKind.OperationStatement: + return "op"; + case SyntaxKind.ModelStatement: + return "model"; + case SyntaxKind.ScalarStatement: + return "scalar"; + case SyntaxKind.InterfaceStatement: + return "interface"; + case SyntaxKind.UnionStatement: + return "union"; + case SyntaxKind.EnumStatement: + return "enum"; + case SyntaxKind.AliasStatement: + return "alias"; + case SyntaxKind.DecoratorDeclarationStatement: + return "dec"; + case SyntaxKind.FunctionDeclarationStatement: + return "function"; + case SyntaxKind.ConstStatement: + return "const"; + default: + compilerAssert(false, `Unknown declaration kind: ${nodeKind}`); + } +} diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 0664ba9874d..10ee0f3053d 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -2,6 +2,7 @@ import { isArray, mutate } from "../utils/misc.js"; import { codePointBefore, isIdentifierContinue, trim } from "./charcode.js"; import { compilerAssert } from "./diagnostics.js"; import { CompilerDiagnostics, createDiagnostic } from "./messages.js"; +import { modifiersToFlags } from "./modifiers.js"; import { createScanner, isComment, @@ -2187,21 +2188,6 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } - function modifiersToFlags(modifiers: Modifier[]): ModifierFlags { - let flags = ModifierFlags.None; - for (const modifier of modifiers) { - switch (modifier.kind) { - case SyntaxKind.ExternKeyword: - flags |= ModifierFlags.Extern; - break; - case SyntaxKind.InternalKeyword: - flags |= ModifierFlags.Internal; - break; - } - } - return flags; - } - function parseRange(mode: ParseMode, range: TextRange, callback: () => T): T { const savedMode = currentMode; const result = scanner.scanRange(range, () => { From d52144dc622ee9719a35062788f7d9932963520a Mon Sep 17 00:00:00 2001 From: Will Temple Date: Tue, 17 Feb 2026 12:19:34 -0500 Subject: [PATCH 03/14] 'internal' keyword hilighting --- packages/compiler/src/core/scanner.ts | 5 ++--- packages/compiler/src/server/completion.ts | 1 + packages/compiler/test/server/colorization.test.ts | 1 + packages/compiler/test/server/completion.test.ts | 1 + packages/monarch/src/typespec-monarch.ts | 1 + 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/core/scanner.ts b/packages/compiler/src/core/scanner.ts index ebbd4db61bc..c6a7ba92652 100644 --- a/packages/compiler/src/core/scanner.ts +++ b/packages/compiler/src/core/scanner.ts @@ -139,6 +139,7 @@ export enum Token { /** @internal */ __StartModifierKeyword = __EndStatementKeyword, ExternKeyword = __StartModifierKeyword, + InternalKeyword, /** @internal */ __EndModifierKeyword, /////////////////////////////////////////////////////////////// @@ -198,7 +199,6 @@ export enum Token { PrivateKeyword, PublicKeyword, ProtectedKeyword, - InternalKeyword, SealedKeyword, LocalKeyword, AsyncKeyword, @@ -383,6 +383,7 @@ export const Keywords: ReadonlyMap = new Map([ ["never", Token.NeverKeyword], ["unknown", Token.UnknownKeyword], ["extern", Token.ExternKeyword], + ["internal", Token.InternalKeyword], // Reserved keywords ["statemachine", Token.StatemachineKeyword], @@ -420,7 +421,6 @@ export const Keywords: ReadonlyMap = new Map([ ["private", Token.PrivateKeyword], ["public", Token.PublicKeyword], ["protected", Token.ProtectedKeyword], - ["internal", Token.InternalKeyword], ["sealed", Token.SealedKeyword], ["local", Token.LocalKeyword], ["async", Token.AsyncKeyword], @@ -460,7 +460,6 @@ export const ReservedKeywords: ReadonlyMap = new Map([ ["private", Token.PrivateKeyword], ["public", Token.PublicKeyword], ["protected", Token.ProtectedKeyword], - ["internal", Token.InternalKeyword], ["sealed", Token.SealedKeyword], ["local", Token.LocalKeyword], ["async", Token.AsyncKeyword], diff --git a/packages/compiler/src/server/completion.ts b/packages/compiler/src/server/completion.ts index 0f48ccb3c76..03a8278e679 100644 --- a/packages/compiler/src/server/completion.ts +++ b/packages/compiler/src/server/completion.ts @@ -238,6 +238,7 @@ const keywords = [ // Modifiers ["extern", { root: true, namespace: true }], + ["internal", { root: true, namespace: true }], // Scalars ["init", { scalarBody: true }], diff --git a/packages/compiler/test/server/colorization.test.ts b/packages/compiler/test/server/colorization.test.ts index 772710b0551..af9aeaa05b6 100644 --- a/packages/compiler/test/server/colorization.test.ts +++ b/packages/compiler/test/server/colorization.test.ts @@ -44,6 +44,7 @@ const Token = { fn: createToken("fn", "keyword.other.tsp"), extends: createToken("extends", "keyword.other.tsp"), extern: createToken("extern", "keyword.other.tsp"), + internal: createToken("internal", "keyword.other.tsp"), is: createToken("is", "keyword.other.tsp"), valueof: createToken("valueof", "keyword.other.tsp"), typeof: createToken("typeof", "keyword.other.tsp"), diff --git a/packages/compiler/test/server/completion.test.ts b/packages/compiler/test/server/completion.test.ts index aca984ec5ee..c5cfe726900 100644 --- a/packages/compiler/test/server/completion.test.ts +++ b/packages/compiler/test/server/completion.test.ts @@ -20,6 +20,7 @@ describe("complete statement keywords", () => { ["model", true], ["op", true], ["extern", true], + ["internal", true], ["dec", true], ["alias", true], ["namespace", true], diff --git a/packages/monarch/src/typespec-monarch.ts b/packages/monarch/src/typespec-monarch.ts index 534fb61e402..60de98e468e 100644 --- a/packages/monarch/src/typespec-monarch.ts +++ b/packages/monarch/src/typespec-monarch.ts @@ -29,6 +29,7 @@ const keywords = [ "projection", "dec", "extern", + "internal", "fn", ]; const namedLiterals = ["true", "false", "null", "unknown", "never"]; From b39b66c784223e689e4cf53dc7f06e0fa582ad6f Mon Sep 17 00:00:00 2001 From: Will Temple Date: Tue, 17 Feb 2026 12:56:06 -0500 Subject: [PATCH 04/14] Warn, test --- packages/compiler/src/core/binder.ts | 6 +- packages/compiler/src/core/checker.ts | 39 +- packages/compiler/src/core/messages.ts | 7 + packages/compiler/src/core/modifiers.ts | 20 +- .../compiler/test/checker/internal.test.ts | 351 ++++++++++++++++++ packages/compiler/test/scanner.test.ts | 1 + 6 files changed, 400 insertions(+), 24 deletions(-) create mode 100644 packages/compiler/test/checker/internal.test.ts diff --git a/packages/compiler/src/core/binder.ts b/packages/compiler/src/core/binder.ts index 5c0b4bfc946..8b005abaf56 100644 --- a/packages/compiler/src/core/binder.ts +++ b/packages/compiler/src/core/binder.ts @@ -469,14 +469,10 @@ export function createBinder(program: Program): Binder { // locals are never shared. mutate(statement).locals = createSymbolTable(); mutate(existingBinding.declarations).push(statement); - - // TODO: report diagnostic if merging an internal and non-internal namespace } else { // Initialize locals for non-exported symbols mutate(statement).locals = createSymbolTable(); - const internal = - statement.modifierFlags & ModifierFlags.Internal ? SymbolFlags.Internal : SymbolFlags.None; - declareSymbol(statement, SymbolFlags.Namespace | SymbolFlags.Declaration | internal); + declareSymbol(statement, SymbolFlags.Namespace | SymbolFlags.Declaration); } currentFile.namespaces.push(statement); diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 6df5a730390..250dcd4ae48 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -2052,14 +2052,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ctx: CheckContext, node: DecoratorDeclarationStatementNode, ): Decorator { - checkModifiers(program, node); - const symbol = getMergedSymbol(node.symbol); const links = getSymbolLinks(symbol); if (links.declaredType && ctx.mapper === undefined) { // we're not instantiating this operation and we've already checked it return links.declaredType as Decorator; } + checkModifiers(program, node); const namespace = getParentNamespaceType(node); compilerAssert( @@ -2454,7 +2453,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker node: OperationStatementNode, parentInterface?: Interface, ): Operation { - checkModifiers(program, node); const inInterface = node.parent?.kind === SyntaxKind.InterfaceStatement; const symbol = inInterface ? getSymbolForMember(node) : node.symbol; const links = symbol && getSymbolLinks(symbol); @@ -2465,6 +2463,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return links.declaredType as Operation; } } + if (ctx.mapper === undefined) { + checkModifiers(program, node); + } if (ctx.mapper === undefined && inInterface) { compilerAssert( @@ -3360,8 +3361,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkSymbolAccess(sourceLocation: LocationContext, node: Node, symbol: Sym | undefined) { if (!symbol) return; - if (symbol.flags & SymbolFlags.Internal) debugger; - const isInternalDeclaration = (symbol.flags & (SymbolFlags.Internal | SymbolFlags.Declaration)) === (SymbolFlags.Internal | SymbolFlags.Declaration); @@ -3856,8 +3855,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkModelStatement(ctx: CheckContext, node: ModelStatementNode): Model { - checkModifiers(program, node); - const links = getSymbolLinks(node.symbol); if (ctx.mapper === undefined && node.templateParameters.length > 0) { @@ -3868,6 +3865,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker // we're not instantiating this model and we've already checked it return links.declaredType as any; } + if (ctx.mapper === undefined) { + checkModifiers(program, node); + } checkTemplateDeclaration(ctx, node); const decorators: DecoratorApplication[] = []; @@ -5516,7 +5516,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkScalar(ctx: CheckContext, node: ScalarStatementNode): Scalar { - checkModifiers(program, node); const links = getSymbolLinks(node.symbol); if (ctx.mapper === undefined && node.templateParameters.length > 0) { @@ -5528,6 +5527,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker // we're not instantiating this model and we've already checked it return links.declaredType as any; } + if (ctx.mapper === undefined) { + checkModifiers(program, node); + } checkTemplateDeclaration(ctx, node); const decorators: DecoratorApplication[] = []; @@ -5666,8 +5668,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkAlias(ctx: CheckContext, node: AliasStatementNode): Type | IndeterminateEntity { - checkModifiers(program, node); - const links = getSymbolLinks(node.symbol); if (ctx.mapper === undefined && node.templateParameters.length > 0) { @@ -5678,6 +5678,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker if (links.declaredType && ctx.mapper === undefined) { return links.declaredType; } + if (ctx.mapper === undefined) { + checkModifiers(program, node); + } checkTemplateDeclaration(ctx, node); const aliasSymId = getNodeSym(node); @@ -5713,12 +5716,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkConst(node: ConstStatementNode): Value | null { - checkModifiers(program, node); - const links = getSymbolLinks(node.symbol); if (links.value !== undefined) { return links.value; } + checkModifiers(program, node); const type = node.type ? getTypeForNode(node.type, undefined) : undefined; @@ -5764,10 +5766,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkEnum(ctx: CheckContext, node: EnumStatementNode): Type { - checkModifiers(program, node); - const links = getSymbolLinks(node.symbol); if (!links.type) { + checkModifiers(program, node); const enumType: Enum = (links.type = createType({ kind: "Enum", name: node.id.sv, @@ -5820,8 +5821,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkInterface(ctx: CheckContext, node: InterfaceStatementNode): Interface { - checkModifiers(program, node); - const links = getSymbolLinks(node.symbol); if (ctx.mapper === undefined && node.templateParameters.length > 0) { @@ -5833,6 +5832,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker // we're not instantiating this interface and we've already checked it return links.declaredType as Interface; } + if (ctx.mapper === undefined) { + checkModifiers(program, node); + } checkTemplateDeclaration(ctx, node); const interfaceType: Interface = createType({ @@ -5937,8 +5939,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkUnion(ctx: CheckContext, node: UnionStatementNode) { - checkModifiers(program, node); - const links = getSymbolLinks(node.symbol); if (ctx.mapper === undefined && node.templateParameters.length > 0) { @@ -5949,6 +5949,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker // we're not instantiating this union and we've already checked it return links.declaredType as Union; } + if (ctx.mapper === undefined) { + checkModifiers(program, node); + } checkTemplateDeclaration(ctx, node); const variants = createRekeyableMap(); diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index f2c22e01f0f..9c48af2a351 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -542,6 +542,13 @@ const diagnostics = { "not-allowed": paramMessage`Modifier '${"modifier"}' cannot be used on declarations of type '${"nodeKind"}'.`, }, }, + "experimental-internal": { + severity: "warning", + messages: { + default: + "The 'internal' modifier is experimental and may change or be removed in future releases.", + }, + }, "function-unsupported": { severity: "error", messages: { diff --git a/packages/compiler/src/core/modifiers.ts b/packages/compiler/src/core/modifiers.ts index 8ac383ce16d..1e10cda5bba 100644 --- a/packages/compiler/src/core/modifiers.ts +++ b/packages/compiler/src/core/modifiers.ts @@ -27,8 +27,13 @@ const DEFAULT_COMPATIBILITY: ModifierCompatibility = { required: ModifierFlags.None, }; +const NO_MODIFIERS: ModifierCompatibility = { + allowed: ModifierFlags.None, + required: ModifierFlags.None, +}; + const SYNTAX_MODIFIERS: Readonly> = { - [SyntaxKind.NamespaceStatement]: DEFAULT_COMPATIBILITY, + [SyntaxKind.NamespaceStatement]: NO_MODIFIERS, [SyntaxKind.OperationStatement]: DEFAULT_COMPATIBILITY, [SyntaxKind.ModelStatement]: DEFAULT_COMPATIBILITY, [SyntaxKind.ScalarStatement]: DEFAULT_COMPATIBILITY, @@ -58,6 +63,19 @@ export function checkModifiers(program: Program, node: Declaration): boolean { let isValid = true; + // Emit experimental warning for any use of the 'internal' modifier. + if (node.modifierFlags & ModifierFlags.Internal) { + const internalModifiers = filterModifiersByFlags(node.modifiers, ModifierFlags.Internal); + for (const modifier of internalModifiers) { + program.reportDiagnostic( + createDiagnostic({ + code: "experimental-internal", + target: modifier, + }), + ); + } + } + const invalidModifiers = node.modifierFlags & ~compatibility.allowed; if (invalidModifiers) { diff --git a/packages/compiler/test/checker/internal.test.ts b/packages/compiler/test/checker/internal.test.ts new file mode 100644 index 00000000000..2a9ff19bb0a --- /dev/null +++ b/packages/compiler/test/checker/internal.test.ts @@ -0,0 +1,351 @@ +import { describe, it } from "vitest"; +import { expectDiagnosticEmpty, expectDiagnostics } from "../../src/testing/index.js"; +import { Tester } from "../tester.js"; + +describe("compiler: internal modifier", () => { + describe("modifier validation", () => { + const declarationKinds = [ + { keyword: "model", code: "internal model Foo {}" }, + { keyword: "scalar", code: "internal scalar Foo;" }, + { keyword: "interface", code: "internal interface Foo {}" }, + { keyword: "union", code: "internal union Foo {}" }, + { keyword: "op", code: "internal op foo(): void;" }, + { keyword: "enum", code: "internal enum Foo {}" }, + { keyword: "alias", code: "internal alias Foo = string;" }, + { keyword: "const", code: "internal const foo = 1;" }, + ]; + + for (const { keyword, code } of declarationKinds) { + it(`allows 'internal' on ${keyword} declaration (with experimental warning)`, async () => { + const diagnostics = await Tester.diagnose(code); + expectDiagnostics(diagnostics, { + code: "experimental-internal", + severity: "warning", + message: + "The 'internal' modifier is experimental and may change or be removed in future releases.", + }); + }); + } + + it("allows 'internal' combined with 'extern' on decorator declaration", async () => { + const diagnostics = await Tester.files({ + "test.js": { kind: "js", exports: { $myDec: () => {} } }, + }) + .import("./test.js") + .diagnose(`internal extern dec myDec(target: unknown);`); + + // Only the experimental warning, no error + expectDiagnostics(diagnostics, { + code: "experimental-internal", + }); + }); + + it("does not allow 'internal' on namespace", async () => { + const diagnostics = await Tester.diagnose(`internal namespace Foo {}`); + expectDiagnostics(diagnostics, [ + { + code: "experimental-internal", + }, + { + code: "invalid-modifier", + message: "Modifier 'internal' cannot be used on declarations of type 'namespace'.", + }, + ]); + }); + + it("does not allow 'internal' on blockless namespace", async () => { + const diagnostics = await Tester.diagnose(`internal namespace Foo;`); + expectDiagnostics(diagnostics, [ + { + code: "experimental-internal", + }, + { + code: "invalid-modifier", + message: "Modifier 'internal' cannot be used on declarations of type 'namespace'.", + }, + ]); + }); + + it("does not emit experimental warning without 'internal' modifier", async () => { + const diagnostics = await Tester.diagnose(`model Foo {}`); + expectDiagnosticEmpty(diagnostics); + }); + }); + + describe("access control", () => { + function createLibraryTester(libFiles: Record) { + const files: Record = { + "node_modules/my-lib/package.json": JSON.stringify({ + name: "my-lib", + version: "1.0.0", + exports: { ".": { typespec: "./main.tsp" } }, + }), + }; + for (const [name, content] of Object.entries(libFiles)) { + files[`node_modules/my-lib/${name}`] = content; + } + return Tester.files(files); + } + + describe("cross-library access (should fail)", () => { + it("rejects access to internal model from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": "internal model LibModel {}", + }).diagnose(` + import "my-lib"; + model Consumer { x: LibModel } + `); + + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); + + it("rejects access to internal scalar from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": "internal scalar LibScalar;", + }).diagnose(` + import "my-lib"; + model Consumer { x: LibScalar } + `); + + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); + + it("rejects access to internal interface from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": "internal interface LibIface {}", + }).diagnose(` + import "my-lib"; + interface Consumer extends LibIface {} + `); + + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); + + it("rejects access to internal union from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": "internal union LibUnion {}", + }).diagnose(` + import "my-lib"; + model Consumer { x: LibUnion } + `); + + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); + + it("rejects access to internal op from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": "internal op libOp(): void;", + }).diagnose(` + import "my-lib"; + op consumer is libOp; + `); + + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); + + it("rejects access to internal enum from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": "internal enum LibEnum { a, b }", + }).diagnose(` + import "my-lib"; + model Consumer { x: LibEnum } + `); + + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); + + it("rejects access to internal alias from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": ` + model Impl {} + internal alias LibAlias = Impl; + `, + }).diagnose(` + import "my-lib"; + model Consumer { x: LibAlias } + `); + + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); + + it("rejects access to internal model in a namespace from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": ` + namespace MyLib; + internal model Secret {} + `, + }).diagnose(` + import "my-lib"; + model Consumer { x: MyLib.Secret } + `); + + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); + + it("rejects access to internal model via 'using' from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": ` + namespace MyLib; + internal model Secret {} + `, + }).diagnose(` + import "my-lib"; + using MyLib; + model Consumer { x: Secret } + `); + + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); + + it("rejects extending an internal model from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": "internal model Base { x: string }", + }).diagnose(` + import "my-lib"; + model Consumer extends Base {} + `); + + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); + }); + + describe("same-project access (should succeed)", () => { + it("allows access to internal model within the same project", async () => { + const diagnostics = await Tester.diagnose(` + internal model Secret {} + model Consumer { x: Secret } + `); + + // Only the experimental warning, no access error + expectDiagnostics(diagnostics, { code: "experimental-internal" }); + }); + + it("allows access to internal enum within the same project", async () => { + const diagnostics = await Tester.diagnose(` + internal enum Status { active, inactive } + model Consumer { x: Status } + `); + + expectDiagnostics(diagnostics, { code: "experimental-internal" }); + }); + + it("allows access to internal model across files in the same project", async () => { + const [, diagnostics] = await Tester.compileAndDiagnose({ + "main.tsp": ` + import "./other.tsp"; + model Consumer { x: Secret } + `, + "other.tsp": ` + internal model Secret {} + `, + }); + + expectDiagnostics(diagnostics, { code: "experimental-internal" }); + }); + + it("allows access to internal op within the same project", async () => { + const diagnostics = await Tester.diagnose(` + internal op helper(): void; + op consumer is helper; + `); + + expectDiagnostics(diagnostics, { code: "experimental-internal" }); + }); + + it("allows access to internal scalar within the same project", async () => { + const diagnostics = await Tester.diagnose(` + internal scalar MyScalar; + model Consumer { x: MyScalar } + `); + + expectDiagnostics(diagnostics, { code: "experimental-internal" }); + }); + + it("allows access to internal alias within the same project", async () => { + const diagnostics = await Tester.diagnose(` + internal alias Shorthand = string; + model Consumer { x: Shorthand } + `); + + expectDiagnostics(diagnostics, { code: "experimental-internal" }); + }); + }); + + describe("same-library access (should succeed)", () => { + it("allows access to internal model within the same library", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": ` + import "./helper.tsp"; + model Public { x: InternalHelper } + `, + "helper.tsp": ` + internal model InternalHelper {} + `, + }).diagnose(` + import "my-lib"; + model Consumer { x: Public } + `); + + // experimental-internal for InternalHelper in the library, no access error + expectDiagnostics(diagnostics, { code: "experimental-internal" }); + }); + }); + + describe("public symbols from library (should succeed)", () => { + it("allows access to non-internal model from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": "model PublicModel {}", + }).diagnose(` + import "my-lib"; + model Consumer { x: PublicModel } + `); + + expectDiagnosticEmpty(diagnostics); + }); + + it("allows access to non-internal model in a namespace from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": ` + namespace MyLib; + model PublicModel {} + `, + }).diagnose(` + import "my-lib"; + model Consumer { x: MyLib.PublicModel } + `); + + expectDiagnosticEmpty(diagnostics); + }); + }); + }); +}); diff --git a/packages/compiler/test/scanner.test.ts b/packages/compiler/test/scanner.test.ts index e2cb213801c..3579ceb1fa3 100644 --- a/packages/compiler/test/scanner.test.ts +++ b/packages/compiler/test/scanner.test.ts @@ -396,6 +396,7 @@ describe("compiler: scanner", () => { Token.NeverKeyword, Token.UnknownKeyword, Token.ExternKeyword, + Token.InternalKeyword, Token.ValueOfKeyword, Token.TypeOfKeyword, ]; From 1dceabc18d72b7949ced366ac8af2da7250a50ad Mon Sep 17 00:00:00 2001 From: Will Temple Date: Tue, 17 Feb 2026 13:18:54 -0500 Subject: [PATCH 05/14] Ensure works as identifier --- packages/compiler/src/core/parser.ts | 15 +++++++++++---- packages/compiler/test/checker/internal.test.ts | 12 ++++++++++++ packages/compiler/test/parser.test.ts | 17 +++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index a09768da0b3..4045c34195d 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -7,6 +7,7 @@ import { createScanner, isComment, isKeyword, + isModifier, isPunctuation, isReservedKeyword, isStatementKeyword, @@ -745,11 +746,15 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const nextToken = token(); let id: IdentifierNode | undefined; - if (isReservedKeyword(nextToken)) { + if (isReservedKeyword(nextToken) || isModifier(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 } }); + if (isReservedKeyword(nextToken)) { + error({ code: "reserved-identifier", messageId: "future", format: { name: id.sv } }); + } else { + error({ code: "reserved-identifier" }); + } } return { kind: SyntaxKind.TypeReference, @@ -1999,8 +2004,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa allowReservedIdentifier?: boolean; }): IdentifierNode { if (isKeyword(token())) { - error({ code: "reserved-identifier" }); - return createMissingIdentifier(); + if (!(isModifier(token()) && options?.allowReservedIdentifier)) { + error({ code: "reserved-identifier" }); + return createMissingIdentifier(); + } } else if (isReservedKeyword(token())) { if (!options?.allowReservedIdentifier) { error({ code: "reserved-identifier", messageId: "future", format: { name: tokenValue() } }); diff --git a/packages/compiler/test/checker/internal.test.ts b/packages/compiler/test/checker/internal.test.ts index 2a9ff19bb0a..37cbeb27d86 100644 --- a/packages/compiler/test/checker/internal.test.ts +++ b/packages/compiler/test/checker/internal.test.ts @@ -348,4 +348,16 @@ describe("compiler: internal modifier", () => { }); }); }); + + describe("'internal' as identifier", () => { + it("allows 'internal' as a model property name", async () => { + const diagnostics = await Tester.diagnose(`model M { internal: string; }`); + expectDiagnosticEmpty(diagnostics); + }); + + it("allows 'internal' as a union variant name", async () => { + const diagnostics = await Tester.diagnose(`union U { internal: string }`); + expectDiagnosticEmpty(diagnostics); + }); + }); }); diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index 1e8d360c19d..fe55dadb8e9 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -51,6 +51,23 @@ describe("compiler: parser", () => { ); }); + describe("modifier keywords as identifiers", () => { + const modifiers = ["internal", "extern"]; + + // Allowed as members + parseEach(modifiers.map((x) => `model Foo { ${x}: string }`)); + parseEach(modifiers.map((x) => `union Foo { ${x}: string }`)); + parseEach(modifiers.map((x) => `const a = #{ ${x}: string };`)); + + // Error when used as declaration name + parseErrorEach( + modifiers.map((x) => [ + `model ${x} {}`, + [{ message: /Keyword cannot be used as identifier/ }], + ]), + ); + }); + describe("import statements", () => { parseEach(['import "x";']); From 2f299d79f228456d61e491e8267668f366934422 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Tue, 17 Feb 2026 14:15:53 -0500 Subject: [PATCH 06/14] Fix a couple of tests. --- .../compiler/test/checker/internal.test.ts | 461 +++++++++--------- 1 file changed, 233 insertions(+), 228 deletions(-) diff --git a/packages/compiler/test/checker/internal.test.ts b/packages/compiler/test/checker/internal.test.ts index 37cbeb27d86..4d3eb6d7423 100644 --- a/packages/compiler/test/checker/internal.test.ts +++ b/packages/compiler/test/checker/internal.test.ts @@ -2,362 +2,367 @@ import { describe, it } from "vitest"; import { expectDiagnosticEmpty, expectDiagnostics } from "../../src/testing/index.js"; import { Tester } from "../tester.js"; -describe("compiler: internal modifier", () => { - describe("modifier validation", () => { - const declarationKinds = [ - { keyword: "model", code: "internal model Foo {}" }, - { keyword: "scalar", code: "internal scalar Foo;" }, - { keyword: "interface", code: "internal interface Foo {}" }, - { keyword: "union", code: "internal union Foo {}" }, - { keyword: "op", code: "internal op foo(): void;" }, - { keyword: "enum", code: "internal enum Foo {}" }, - { keyword: "alias", code: "internal alias Foo = string;" }, - { keyword: "const", code: "internal const foo = 1;" }, - ]; - - for (const { keyword, code } of declarationKinds) { - it(`allows 'internal' on ${keyword} declaration (with experimental warning)`, async () => { - const diagnostics = await Tester.diagnose(code); - expectDiagnostics(diagnostics, { - code: "experimental-internal", - severity: "warning", - message: - "The 'internal' modifier is experimental and may change or be removed in future releases.", - }); - }); - } - - it("allows 'internal' combined with 'extern' on decorator declaration", async () => { - const diagnostics = await Tester.files({ - "test.js": { kind: "js", exports: { $myDec: () => {} } }, - }) - .import("./test.js") - .diagnose(`internal extern dec myDec(target: unknown);`); - - // Only the experimental warning, no error +describe("modifier validation", () => { + const declarationKinds = [ + { keyword: "model", code: "internal model Foo {}" }, + { keyword: "scalar", code: "internal scalar Foo;" }, + { keyword: "interface", code: "internal interface Foo {}" }, + { keyword: "union", code: "internal union Foo {}" }, + { keyword: "op", code: "internal op foo(): void;" }, + { keyword: "enum", code: "internal enum Foo {}" }, + { keyword: "alias", code: "internal alias Foo = string;" }, + { keyword: "const", code: "internal const foo = 1;" }, + ]; + + for (const { keyword, code } of declarationKinds) { + it(`allows 'internal' on ${keyword} declaration (with experimental warning)`, async () => { + const diagnostics = await Tester.diagnose(code); expectDiagnostics(diagnostics, { code: "experimental-internal", + severity: "warning", + message: + "The 'internal' modifier is experimental and may change or be removed in future releases.", }); }); - - it("does not allow 'internal' on namespace", async () => { - const diagnostics = await Tester.diagnose(`internal namespace Foo {}`); - expectDiagnostics(diagnostics, [ - { - code: "experimental-internal", - }, - { - code: "invalid-modifier", - message: "Modifier 'internal' cannot be used on declarations of type 'namespace'.", - }, - ]); + } + + it("allows 'internal' combined with 'extern' on decorator declaration", async () => { + const diagnostics = await Tester.files({ + "test.js": { kind: "js", exports: { $myDec: () => {} } }, + }) + .import("./test.js") + .diagnose(`internal extern dec myDec(target: unknown);`); + + // Only the experimental warning, no error + expectDiagnostics(diagnostics, { + code: "experimental-internal", }); + }); - it("does not allow 'internal' on blockless namespace", async () => { - const diagnostics = await Tester.diagnose(`internal namespace Foo;`); - expectDiagnostics(diagnostics, [ - { - code: "experimental-internal", - }, - { - code: "invalid-modifier", - message: "Modifier 'internal' cannot be used on declarations of type 'namespace'.", - }, - ]); - }); + it("does not allow 'internal' on namespace", async () => { + const diagnostics = await Tester.diagnose(`internal namespace Foo {}`); + expectDiagnostics(diagnostics, [ + { + code: "experimental-internal", + }, + { + code: "invalid-modifier", + message: "Modifier 'internal' cannot be used on declarations of type 'namespace'.", + }, + ]); + }); - it("does not emit experimental warning without 'internal' modifier", async () => { - const diagnostics = await Tester.diagnose(`model Foo {}`); - expectDiagnosticEmpty(diagnostics); - }); + it("does not allow 'internal' on blockless namespace", async () => { + const diagnostics = await Tester.diagnose(`internal namespace Foo;`); + expectDiagnostics(diagnostics, [ + { + code: "experimental-internal", + }, + { + code: "invalid-modifier", + message: "Modifier 'internal' cannot be used on declarations of type 'namespace'.", + }, + ]); }); - describe("access control", () => { - function createLibraryTester(libFiles: Record) { - const files: Record = { - "node_modules/my-lib/package.json": JSON.stringify({ - name: "my-lib", - version: "1.0.0", - exports: { ".": { typespec: "./main.tsp" } }, - }), - }; - for (const [name, content] of Object.entries(libFiles)) { - files[`node_modules/my-lib/${name}`] = content; - } - return Tester.files(files); - } + it("does not emit experimental warning without 'internal' modifier", async () => { + const diagnostics = await Tester.diagnose(`model Foo {}`); + expectDiagnosticEmpty(diagnostics); + }); +}); - describe("cross-library access (should fail)", () => { - it("rejects access to internal model from another package", async () => { - const diagnostics = await createLibraryTester({ - "main.tsp": "internal model LibModel {}", - }).diagnose(` +describe("access control", () => { + function createLibraryTester(libFiles: Record) { + const files: Record = { + "node_modules/my-lib/package.json": JSON.stringify({ + name: "my-lib", + version: "1.0.0", + exports: { ".": { typespec: "./main.tsp" } }, + }), + }; + for (const [name, content] of Object.entries(libFiles)) { + files[`node_modules/my-lib/${name}`] = content; + } + return Tester.files(files); + } + + describe("cross-library access", () => { + it("rejects access to internal model from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": "internal model LibModel {}", + }).diagnose(` import "my-lib"; model Consumer { x: LibModel } `); - expectDiagnostics(diagnostics, [ - { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, - ]); - }); + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); - it("rejects access to internal scalar from another package", async () => { - const diagnostics = await createLibraryTester({ - "main.tsp": "internal scalar LibScalar;", - }).diagnose(` + it("rejects access to internal scalar from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": "internal scalar LibScalar;", + }).diagnose(` import "my-lib"; model Consumer { x: LibScalar } `); - expectDiagnostics(diagnostics, [ - { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, - ]); - }); + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); - it("rejects access to internal interface from another package", async () => { - const diagnostics = await createLibraryTester({ - "main.tsp": "internal interface LibIface {}", - }).diagnose(` + it("rejects access to internal interface from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": "internal interface LibIface {}", + }).diagnose(` import "my-lib"; interface Consumer extends LibIface {} `); - expectDiagnostics(diagnostics, [ - { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, - ]); - }); + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); - it("rejects access to internal union from another package", async () => { - const diagnostics = await createLibraryTester({ - "main.tsp": "internal union LibUnion {}", - }).diagnose(` + it("rejects access to internal union from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": "internal union LibUnion {}", + }).diagnose(` import "my-lib"; model Consumer { x: LibUnion } `); - expectDiagnostics(diagnostics, [ - { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, - ]); - }); + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); - it("rejects access to internal op from another package", async () => { - const diagnostics = await createLibraryTester({ - "main.tsp": "internal op libOp(): void;", - }).diagnose(` + it("rejects access to internal op from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": "internal op libOp(): void;", + }).diagnose(` import "my-lib"; op consumer is libOp; `); - expectDiagnostics(diagnostics, [ - { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, - ]); - }); + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); - it("rejects access to internal enum from another package", async () => { - const diagnostics = await createLibraryTester({ - "main.tsp": "internal enum LibEnum { a, b }", - }).diagnose(` + it("rejects access to internal enum from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": "internal enum LibEnum { a, b }", + }).diagnose(` import "my-lib"; model Consumer { x: LibEnum } `); - expectDiagnostics(diagnostics, [ - { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, - ]); - }); + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); - it("rejects access to internal alias from another package", async () => { - const diagnostics = await createLibraryTester({ - "main.tsp": ` + it("rejects access to internal alias from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": ` model Impl {} internal alias LibAlias = Impl; `, - }).diagnose(` + }).diagnose(` import "my-lib"; model Consumer { x: LibAlias } `); - expectDiagnostics(diagnostics, [ - { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, - ]); - }); + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); - it("rejects access to internal model in a namespace from another package", async () => { - const diagnostics = await createLibraryTester({ - "main.tsp": ` + it("rejects access to internal model in a namespace from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": ` namespace MyLib; internal model Secret {} `, - }).diagnose(` + }).diagnose(` import "my-lib"; model Consumer { x: MyLib.Secret } `); - expectDiagnostics(diagnostics, [ - { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, - ]); - }); + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); - it("rejects access to internal model via 'using' from another package", async () => { - const diagnostics = await createLibraryTester({ - "main.tsp": ` + it("rejects access to internal model via 'using' from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": ` namespace MyLib; internal model Secret {} `, - }).diagnose(` + }).diagnose(` import "my-lib"; using MyLib; model Consumer { x: Secret } `); - expectDiagnostics(diagnostics, [ - { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, - ]); - }); + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); + }); - it("rejects extending an internal model from another package", async () => { - const diagnostics = await createLibraryTester({ - "main.tsp": "internal model Base { x: string }", - }).diagnose(` + it("rejects extending an internal model from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": "internal model Base { x: string }", + }).diagnose(` import "my-lib"; model Consumer extends Base {} `); - expectDiagnostics(diagnostics, [ - { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, - ]); - }); + expectDiagnostics(diagnostics, [ + { code: "invalid-ref", message: /internal/ }, + { code: "experimental-internal" }, + ]); }); + }); - describe("same-project access (should succeed)", () => { - it("allows access to internal model within the same project", async () => { - const diagnostics = await Tester.diagnose(` + describe("user-project local access", () => { + it("allows access to internal model within the same project", async () => { + const diagnostics = await Tester.diagnose(` internal model Secret {} model Consumer { x: Secret } `); - // Only the experimental warning, no access error - expectDiagnostics(diagnostics, { code: "experimental-internal" }); - }); + // Only the experimental warning, no access error + expectDiagnostics(diagnostics, { code: "experimental-internal" }); + }); - it("allows access to internal enum within the same project", async () => { - const diagnostics = await Tester.diagnose(` + it("allows access to internal enum within the same project", async () => { + const diagnostics = await Tester.diagnose(` internal enum Status { active, inactive } model Consumer { x: Status } `); - expectDiagnostics(diagnostics, { code: "experimental-internal" }); - }); + expectDiagnostics(diagnostics, { code: "experimental-internal" }); + }); - it("allows access to internal model across files in the same project", async () => { - const [, diagnostics] = await Tester.compileAndDiagnose({ - "main.tsp": ` + it("allows access to internal model across files in the same project", async () => { + const [, diagnostics] = await Tester.compileAndDiagnose({ + "main.tsp": ` import "./other.tsp"; model Consumer { x: Secret } `, - "other.tsp": ` + "other.tsp": ` internal model Secret {} `, - }); - - expectDiagnostics(diagnostics, { code: "experimental-internal" }); }); - it("allows access to internal op within the same project", async () => { - const diagnostics = await Tester.diagnose(` + expectDiagnostics(diagnostics, { code: "experimental-internal" }); + }); + + it("allows access to internal op within the same project", async () => { + const diagnostics = await Tester.diagnose(` internal op helper(): void; op consumer is helper; `); - expectDiagnostics(diagnostics, { code: "experimental-internal" }); - }); + expectDiagnostics(diagnostics, { code: "experimental-internal" }); + }); - it("allows access to internal scalar within the same project", async () => { - const diagnostics = await Tester.diagnose(` + it("allows access to internal scalar within the same project", async () => { + const diagnostics = await Tester.diagnose(` internal scalar MyScalar; model Consumer { x: MyScalar } `); - expectDiagnostics(diagnostics, { code: "experimental-internal" }); - }); + expectDiagnostics(diagnostics, { code: "experimental-internal" }); + }); - it("allows access to internal alias within the same project", async () => { - const diagnostics = await Tester.diagnose(` + it("allows access to internal alias within the same project", async () => { + const diagnostics = await Tester.diagnose(` internal alias Shorthand = string; model Consumer { x: Shorthand } `); - expectDiagnostics(diagnostics, { code: "experimental-internal" }); - }); + expectDiagnostics(diagnostics, { code: "experimental-internal" }); }); + }); - describe("same-library access (should succeed)", () => { - it("allows access to internal model within the same library", async () => { - const diagnostics = await createLibraryTester({ - "main.tsp": ` + describe("same-library access", () => { + it("allows access to internal model within the same library", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": ` import "./helper.tsp"; model Public { x: InternalHelper } `, - "helper.tsp": ` + "helper.tsp": ` internal model InternalHelper {} `, - }).diagnose(` + }).diagnose(` import "my-lib"; model Consumer { x: Public } `); - // experimental-internal for InternalHelper in the library, no access error - expectDiagnostics(diagnostics, { code: "experimental-internal" }); - }); + // experimental-internal for InternalHelper in the library, no access error + expectDiagnostics(diagnostics, { code: "experimental-internal" }); }); + }); + + describe("public symbols from library that reference internal symbols", () => { + it("allows access to non-internal model from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": ` + internal model InternalModel {} - describe("public symbols from library (should succeed)", () => { - it("allows access to non-internal model from another package", async () => { - const diagnostics = await createLibraryTester({ - "main.tsp": "model PublicModel {}", - }).diagnose(` + model PublicModel { prop: InternalModel; } + `, + }).diagnose(` import "my-lib"; model Consumer { x: PublicModel } `); - expectDiagnosticEmpty(diagnostics); - }); + expectDiagnosticEmpty(diagnostics); + }); - it("allows access to non-internal model in a namespace from another package", async () => { - const diagnostics = await createLibraryTester({ - "main.tsp": ` - namespace MyLib; - model PublicModel {} + it("allows access to non-internal model in a namespace from another package", async () => { + const diagnostics = await createLibraryTester({ + "main.tsp": ` + internal model InternalModel {} + + namespace MyLib { + model PublicModel { prop: InternalModel; } + } `, - }).diagnose(` + }).diagnose(` import "my-lib"; model Consumer { x: MyLib.PublicModel } `); - expectDiagnosticEmpty(diagnostics); - }); + expectDiagnosticEmpty(diagnostics); }); }); +}); - describe("'internal' as identifier", () => { - it("allows 'internal' as a model property name", async () => { - const diagnostics = await Tester.diagnose(`model M { internal: string; }`); - expectDiagnosticEmpty(diagnostics); - }); +describe("'internal' as identifier", () => { + it("allows 'internal' as a model property name", async () => { + const diagnostics = await Tester.diagnose(`model M { internal: string; }`); + expectDiagnosticEmpty(diagnostics); + }); - it("allows 'internal' as a union variant name", async () => { - const diagnostics = await Tester.diagnose(`union U { internal: string }`); - expectDiagnosticEmpty(diagnostics); - }); + it("allows 'internal' as a union variant name", async () => { + const diagnostics = await Tester.diagnose(`union U { internal: string }`); + expectDiagnosticEmpty(diagnostics); }); }); From 0babf3f16f57f9c52b23250b4b41e5724ce9ccf4 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Tue, 17 Feb 2026 14:30:18 -0500 Subject: [PATCH 07/14] Fix tests a bit --- packages/compiler/src/core/messages.ts | 6 +-- packages/compiler/src/core/modifiers.ts | 5 +- .../compiler/test/checker/internal.test.ts | 51 +++++++++---------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 9c48af2a351..43e967aefc1 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -542,11 +542,11 @@ const diagnostics = { "not-allowed": paramMessage`Modifier '${"modifier"}' cannot be used on declarations of type '${"nodeKind"}'.`, }, }, - "experimental-internal": { + "experimental-feature": { severity: "warning", messages: { - default: - "The 'internal' modifier is experimental and may change or be removed in future releases.", + default: paramMessage`The '${"featureName"}' feature is experimental and may be changed or removed in a future release. Use with caution.`, + internal: `Internal symbols are experimental and may be changed in a future release. Use with caution. Suppress this message ('#suppress "experimental-feature"') to silence this warning.`, }, }, "function-unsupported": { diff --git a/packages/compiler/src/core/modifiers.ts b/packages/compiler/src/core/modifiers.ts index 1e10cda5bba..05966b4ac4e 100644 --- a/packages/compiler/src/core/modifiers.ts +++ b/packages/compiler/src/core/modifiers.ts @@ -69,8 +69,9 @@ export function checkModifiers(program: Program, node: Declaration): boolean { for (const modifier of internalModifiers) { program.reportDiagnostic( createDiagnostic({ - code: "experimental-internal", - target: modifier, + code: "experimental-feature", + messageId: "internal", + target: node, }), ); } diff --git a/packages/compiler/test/checker/internal.test.ts b/packages/compiler/test/checker/internal.test.ts index 4d3eb6d7423..3701355f91e 100644 --- a/packages/compiler/test/checker/internal.test.ts +++ b/packages/compiler/test/checker/internal.test.ts @@ -18,10 +18,9 @@ describe("modifier validation", () => { it(`allows 'internal' on ${keyword} declaration (with experimental warning)`, async () => { const diagnostics = await Tester.diagnose(code); expectDiagnostics(diagnostics, { - code: "experimental-internal", + code: "experimental-feature", severity: "warning", - message: - "The 'internal' modifier is experimental and may change or be removed in future releases.", + message: `Internal symbols are experimental and may be changed in a future release. Use with caution. Suppress this message ('#suppress "experimental-feature"') to silence this warning.`, }); }); } @@ -35,7 +34,7 @@ describe("modifier validation", () => { // Only the experimental warning, no error expectDiagnostics(diagnostics, { - code: "experimental-internal", + code: "experimental-feature", }); }); @@ -43,7 +42,7 @@ describe("modifier validation", () => { const diagnostics = await Tester.diagnose(`internal namespace Foo {}`); expectDiagnostics(diagnostics, [ { - code: "experimental-internal", + code: "experimental-feature", }, { code: "invalid-modifier", @@ -56,7 +55,7 @@ describe("modifier validation", () => { const diagnostics = await Tester.diagnose(`internal namespace Foo;`); expectDiagnostics(diagnostics, [ { - code: "experimental-internal", + code: "experimental-feature", }, { code: "invalid-modifier", @@ -97,7 +96,7 @@ describe("access control", () => { expectDiagnostics(diagnostics, [ { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, + { code: "experimental-feature" }, ]); }); @@ -111,7 +110,7 @@ describe("access control", () => { expectDiagnostics(diagnostics, [ { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, + { code: "experimental-feature" }, ]); }); @@ -125,7 +124,7 @@ describe("access control", () => { expectDiagnostics(diagnostics, [ { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, + { code: "experimental-feature" }, ]); }); @@ -139,7 +138,7 @@ describe("access control", () => { expectDiagnostics(diagnostics, [ { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, + { code: "experimental-feature" }, ]); }); @@ -153,7 +152,7 @@ describe("access control", () => { expectDiagnostics(diagnostics, [ { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, + { code: "experimental-feature" }, ]); }); @@ -167,7 +166,7 @@ describe("access control", () => { expectDiagnostics(diagnostics, [ { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, + { code: "experimental-feature" }, ]); }); @@ -184,7 +183,7 @@ describe("access control", () => { expectDiagnostics(diagnostics, [ { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, + { code: "experimental-feature" }, ]); }); @@ -201,7 +200,7 @@ describe("access control", () => { expectDiagnostics(diagnostics, [ { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, + { code: "experimental-feature" }, ]); }); @@ -219,7 +218,7 @@ describe("access control", () => { expectDiagnostics(diagnostics, [ { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, + { code: "experimental-feature" }, ]); }); @@ -233,7 +232,7 @@ describe("access control", () => { expectDiagnostics(diagnostics, [ { code: "invalid-ref", message: /internal/ }, - { code: "experimental-internal" }, + { code: "experimental-feature" }, ]); }); }); @@ -246,7 +245,7 @@ describe("access control", () => { `); // Only the experimental warning, no access error - expectDiagnostics(diagnostics, { code: "experimental-internal" }); + expectDiagnostics(diagnostics, { code: "experimental-feature" }); }); it("allows access to internal enum within the same project", async () => { @@ -255,7 +254,7 @@ describe("access control", () => { model Consumer { x: Status } `); - expectDiagnostics(diagnostics, { code: "experimental-internal" }); + expectDiagnostics(diagnostics, { code: "experimental-feature" }); }); it("allows access to internal model across files in the same project", async () => { @@ -269,7 +268,7 @@ describe("access control", () => { `, }); - expectDiagnostics(diagnostics, { code: "experimental-internal" }); + expectDiagnostics(diagnostics, { code: "experimental-feature" }); }); it("allows access to internal op within the same project", async () => { @@ -278,7 +277,7 @@ describe("access control", () => { op consumer is helper; `); - expectDiagnostics(diagnostics, { code: "experimental-internal" }); + expectDiagnostics(diagnostics, { code: "experimental-feature" }); }); it("allows access to internal scalar within the same project", async () => { @@ -287,7 +286,7 @@ describe("access control", () => { model Consumer { x: MyScalar } `); - expectDiagnostics(diagnostics, { code: "experimental-internal" }); + expectDiagnostics(diagnostics, { code: "experimental-feature" }); }); it("allows access to internal alias within the same project", async () => { @@ -296,7 +295,7 @@ describe("access control", () => { model Consumer { x: Shorthand } `); - expectDiagnostics(diagnostics, { code: "experimental-internal" }); + expectDiagnostics(diagnostics, { code: "experimental-feature" }); }); }); @@ -315,8 +314,8 @@ describe("access control", () => { model Consumer { x: Public } `); - // experimental-internal for InternalHelper in the library, no access error - expectDiagnostics(diagnostics, { code: "experimental-internal" }); + // experimental-feature for InternalHelper in the library, no access error + expectDiagnostics(diagnostics, { code: "experimental-feature" }); }); }); @@ -333,7 +332,7 @@ describe("access control", () => { model Consumer { x: PublicModel } `); - expectDiagnosticEmpty(diagnostics); + expectDiagnostics(diagnostics, { code: "experimental-feature" }); }); it("allows access to non-internal model in a namespace from another package", async () => { @@ -350,7 +349,7 @@ describe("access control", () => { model Consumer { x: MyLib.PublicModel } `); - expectDiagnosticEmpty(diagnostics); + expectDiagnostics(diagnostics, { code: "experimental-feature" }); }); }); }); From 3f81fe21a2faf73b73dc49afbf6f324c8921dad6 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 18 Feb 2026 15:00:36 -0500 Subject: [PATCH 08/14] Fix printer --- .../compiler/src/formatter/print/printer.ts | 48 ++++++- website/src/content/current-sidebar.ts | 1 + .../docs/docs/extending-typespec/basics.md | 27 ++++ .../docs/language-basics/access-modifiers.md | 122 ++++++++++++++++++ 4 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 website/src/content/docs/docs/language-basics/access-modifiers.md diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index a7171107c77..0bad80616f4 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -15,6 +15,7 @@ import { CallExpressionNode, Comment, ConstStatementNode, + DeclarationNode, DecoratorDeclarationStatementNode, DecoratorExpressionNode, DirectiveExpressionNode, @@ -335,7 +336,15 @@ export function printAliasStatement( ) { const id = path.call(print, "id"); const template = printTemplateParameters(path, options, print, "templateParameters"); - return ["alias ", id, template, " = ", path.call(print, "value"), ";"]; + return [ + printModifiers(path, options, print), + "alias ", + id, + template, + " = ", + path.call(print, "value"), + ";", + ]; } export function printConstStatement( @@ -346,7 +355,15 @@ export function printConstStatement( const node = path.node; const id = path.call(print, "id"); const type = node.type ? [": ", path.call(print, "type")] : ""; - return ["const ", id, type, " = ", path.call(print, "value"), ";"]; + return [ + printModifiers(path, options, print), + "const ", + id, + type, + " = ", + path.call(print, "value"), + ";", + ]; } export function printCallExpression( @@ -663,7 +680,14 @@ export function printEnumStatement( ) { const { decorators } = printDecorators(path, options, print, { tryInline: false }); const id = path.call(print, "id"); - return [decorators, "enum ", id, " ", printEnumBlock(path, options, print)]; + return [ + decorators, + printModifiers(path, options, print), + "enum ", + id, + " ", + printEnumBlock(path, options, print), + ]; } function printEnumBlock( @@ -710,7 +734,15 @@ export function printUnionStatement( const id = path.call(print, "id"); const { decorators } = printDecorators(path, options, print, { tryInline: false }); const generic = printTemplateParameters(path, options, print, "templateParameters"); - return [decorators, "union ", id, generic, " ", printUnionVariantsBlock(path, options, print)]; + return [ + decorators, + printModifiers(path, options, print), + "union ", + id, + generic, + " ", + printUnionVariantsBlock(path, options, print), + ]; } export function printUnionVariantsBlock( @@ -752,6 +784,7 @@ export function printInterfaceStatement( return [ decorators, + printModifiers(path, options, print), "interface ", id, generic, @@ -1013,6 +1046,7 @@ export function printModelStatement( const body = shouldPrintBody ? [" ", printModelPropertiesBlock(path, options, print)] : ";"; return [ printDecorators(path, options, print, { tryInline: false }).decorators, + printModifiers(path, options, print), "model ", id, generic, @@ -1201,6 +1235,7 @@ function printScalarStatement( const members = shouldPrintBody ? [" ", printScalarBody(path, options, print)] : ";"; return [ printDecorators(path, options, print, { tryInline: false }).decorators, + printModifiers(path, options, print), "scalar ", id, template, @@ -1298,6 +1333,7 @@ export function printOperationStatement( return [ decorators, + printModifiers(path, options, print), inInterface ? "" : "op ", path.call(print, "id"), templateParams, @@ -1505,12 +1541,12 @@ function printFunctionParameterDeclaration( } export function printModifiers( - path: AstPath, + path: AstPath, options: TypeSpecPrettierOptions, print: PrettierChildPrint, ): Doc { const node = path.node; - if (node.modifiers.length === 0) { + if (node.modifiers === undefined || node.modifiers.length === 0) { return ""; } diff --git a/website/src/content/current-sidebar.ts b/website/src/content/current-sidebar.ts index 710fade3f4f..f1a9e42267a 100644 --- a/website/src/content/current-sidebar.ts +++ b/website/src/content/current-sidebar.ts @@ -102,6 +102,7 @@ const sidebar: SidebarItem[] = [ "language-basics/alias", "language-basics/values", "language-basics/type-relations", + "language-basics/access-modifiers", "language-basics/visibility", ], }, diff --git a/website/src/content/docs/docs/extending-typespec/basics.md b/website/src/content/docs/docs/extending-typespec/basics.md index 60a536e537d..3bc8134e2f5 100644 --- a/website/src/content/docs/docs/extending-typespec/basics.md +++ b/website/src/content/docs/docs/extending-typespec/basics.md @@ -181,6 +181,33 @@ model Person { } ``` +## Controlling your library's API surface + +When authoring a library, not every type you define is meant for consumers to use directly. Some types are implementation details that support your library internally. You can use the `internal` modifier to prevent consumers from accessing these types. + +:::caution +The `internal` modifier is an **experimental feature**. See [Access Modifiers](/docs/language-basics/access-modifiers) for full details. +::: + +```typespec +import "../dist/index.js"; + +namespace MyLibrary; + +/** This model is part of the public API. */ +model Person { + name: string; + age: uint8; +} + +/** This model is an implementation detail and cannot be accessed by consumers. */ +internal model PersonValidator { + rules: string[]; +} +``` + +Consumers who try to reference `PersonValidator` will receive a compiler error. This helps you evolve your library's internals without worrying about breaking consumers who might have depended on them. + ## Step 3: Defining dependencies When defining dependencies in a TypeSpec library, follow these rules: diff --git a/website/src/content/docs/docs/language-basics/access-modifiers.md b/website/src/content/docs/docs/language-basics/access-modifiers.md new file mode 100644 index 00000000000..16e1762fcf9 --- /dev/null +++ b/website/src/content/docs/docs/language-basics/access-modifiers.md @@ -0,0 +1,122 @@ +--- +id: access-modifiers +title: Access Modifiers +description: "Language basics - controlling the visibility of declarations across libraries" +llmstxt: true +--- + +Access modifiers control which declarations in a TypeSpec library are accessible to consumers of that library. They allow library authors to distinguish between the public API surface and internal implementation details. + +:::caution +Access modifiers are an **experimental feature** and may change or be removed in a future release. The compiler will emit a warning when access modifiers are used. +::: + +## The `internal` modifier + +The `internal` modifier restricts a declaration so that it can only be accessed within the library or project where it is defined. Consumers of the library cannot reference internal declarations. + +```typespec +internal model MyInternalModel { + secret: string; +} + +model MyPublicModel { + // OK: same library can reference internal models + details: MyInternalModel; +} +``` + +### Supported declarations + +The `internal` modifier can be applied to the following declaration types: + +| Declaration | Example | +| ----------- | ------------------------------ | +| `model` | `internal model Example {}` | +| `scalar` | `internal scalar Example;` | +| `interface` | `internal interface Example {}` | +| `union` | `internal union Example {}` | +| `op` | `internal op example(): void;` | +| `enum` | `internal enum Example {}` | +| `alias` | `internal alias Example = string;` | +| `const` | `internal const example = 1;` | + +:::note +The `internal` modifier **cannot** be applied to `namespace` declarations. +::: + +### Access rules + +The `internal` modifier is a property of the _symbol_ (the name that refers to a type), not a property of the type itself. The compiler prevents code in other packages from _referencing_ an internal symbol by name, but it does not prevent the underlying type from being used indirectly. A public declaration within the same package can freely reference an internal declaration, and the resulting type will be accessible to consumers through the public declaration. + +When a declaration is marked `internal`, the compiler enforces the following rules: + +- **Same library or project**: Code within the same library (or the same project, if not in a library) can reference internal declarations normally. +- **Different library**: Code in a different library that tries to reference an internal declaration by name will receive an error. + +```typespec title="my-lib/main.tsp" +namespace MyLib; + +internal model SecretHelper { + key: string; +} + +model PublicApi { + data: SecretHelper; // ✅ OK: same library +} + +// ✅ OK: a public alias can reference an internal symbol within the same package. +// Consumers can use `ExposedHelper`, even though it refers to the same type as `SecretHelper`. +alias ExposedHelper = SecretHelper; +``` + +```typespec title="consumer/main.tsp" +import "my-lib"; + +model Consumer { + helper: MyLib.SecretHelper; // ❌ Error: SecretHelper is internal + data: MyLib.PublicApi; // ✅ OK: PublicApi is public (even though it references SecretHelper) + exposed: MyLib.ExposedHelper; // ✅ OK: ExposedHelper is a public alias +} +``` + +The error message for a direct reference to an internal symbol will read: + +> Symbol 'SecretHelper' is internal and can only be accessed from within its declaring package. + +### Combining with `extern` + +The `internal` modifier can be combined with the `extern` modifier on decorator declarations to create internal decorator signatures: + +```typespec +internal extern dec myInternalDecorator(target: unknown); +``` + +### Suppressing the experimental warning + +Since access modifiers are currently experimental, using `internal` will emit a warning. You can suppress this warning with a `#suppress` directive: + +```typespec +#suppress "experimental-feature" +internal model MyInternalModel {} +``` + +## Why not `namespace`? + +The `internal` modifier is not supported on namespaces because namespaces in TypeSpec are **open and merged** across files. A namespace declared in one file can be extended in another file — potentially across library boundaries. Applying `internal` to a namespace would create ambiguity about which parts of the namespace are internal and which are public. Instead, mark individual declarations within a namespace as `internal`. + +```typespec +namespace MyLib; + +internal model InternalHelper {} // ✅ Mark individual declarations +model PublicApi {} +``` + +## Relationship to visibility + +The `internal` access modifier is distinct from TypeSpec's [visibility](./visibility.md) system: + +- **Access modifiers** (`internal`) control which _declarations_ (models, operations, etc.) can be referenced across library boundaries. They are enforced at compile time. +- **Visibility** (`@visibility`, `@removeVisibility`) controls which _model properties_ appear in different API operation contexts (e.g., create vs. read). It is a metadata system used by emitters to generate different views of a model. + +These are complementary features — you can use both on the same types. For example, you might have a public model whose properties have different visibility across operations, or an internal model that is only used within your library's implementation. From fabaa413486e2f71e6b7f49074b6280af4bbe955 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 20 Feb 2026 15:09:25 -0500 Subject: [PATCH 09/14] Fixed grammar. --- grammars/typespec.json | 50 +++++++++++++++---- packages/compiler/src/server/tmlanguage.ts | 46 ++++++++++------- .../docs/language-basics/access-modifiers.md | 4 +- 3 files changed, 70 insertions(+), 30 deletions(-) diff --git a/grammars/typespec.json b/grammars/typespec.json index bf191ff4223..ac86e13aa0c 100644 --- a/grammars/typespec.json +++ b/grammars/typespec.json @@ -28,12 +28,15 @@ }, "alias-statement": { "name": "meta.alias-statement.typespec", - "begin": "\\b(alias)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)\\s*", + "begin": "(?:(internal)\\s+)?\\b(alias)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)\\s*", "beginCaptures": { "1": { "name": "keyword.other.tsp" }, "2": { + "name": "keyword.other.tsp" + }, + "3": { "name": "entity.name.type.tsp" } }, @@ -108,12 +111,15 @@ }, "const-statement": { "name": "meta.const-statement.typespec", - "begin": "\\b(const)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", + "begin": "(?:(internal)\\s+)?\\b(const)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", "beginCaptures": { "1": { "name": "keyword.other.tsp" }, "2": { + "name": "keyword.other.tsp" + }, + "3": { "name": "variable.name.tsp" } }, @@ -153,7 +159,7 @@ }, "decorator-declaration-statement": { "name": "meta.decorator-declaration-statement.typespec", - "begin": "(?:(extern)\\s+)?\\b(dec)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", + "begin": "(?:(internal)\\s+)?(?:(extern)\\s+)?\\b(dec)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", "beginCaptures": { "1": { "name": "keyword.other.tsp" @@ -162,6 +168,9 @@ "name": "keyword.other.tsp" }, "3": { + "name": "keyword.other.tsp" + }, + "4": { "name": "entity.name.function.tsp" } }, @@ -323,12 +332,15 @@ }, "enum-statement": { "name": "meta.enum-statement.typespec", - "begin": "\\b(enum)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", + "begin": "(?:(internal)\\s+)?\\b(enum)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", "beginCaptures": { "1": { "name": "keyword.other.tsp" }, "2": { + "name": "keyword.other.tsp" + }, + "3": { "name": "entity.name.type.tsp" } }, @@ -388,7 +400,7 @@ }, "function-declaration-statement": { "name": "meta.function-declaration-statement.typespec", - "begin": "(?:(extern)\\s+)?\\b(fn)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", + "begin": "(?:(internal)\\s+)?(?:(extern)\\s+)?\\b(fn)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", "beginCaptures": { "1": { "name": "keyword.other.tsp" @@ -397,6 +409,9 @@ "name": "keyword.other.tsp" }, "3": { + "name": "keyword.other.tsp" + }, + "4": { "name": "entity.name.function.tsp" } }, @@ -505,10 +520,13 @@ }, "interface-statement": { "name": "meta.interface-statement.typespec", - "begin": "\\b(interface)\\b", + "begin": "(?:(internal)\\s+)?\\b(interface)\\b", "beginCaptures": { "1": { "name": "keyword.other.tsp" + }, + "2": { + "name": "keyword.other.tsp" } }, "end": "(?<=\\})|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", @@ -616,10 +634,13 @@ }, "model-statement": { "name": "meta.model-statement.typespec", - "begin": "\\b(model)\\b", + "begin": "(?:(internal)\\s+)?\\b(model)\\b", "beginCaptures": { "1": { "name": "keyword.other.tsp" + }, + "2": { + "name": "keyword.other.tsp" } }, "end": "(?<=\\})|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", @@ -814,12 +835,15 @@ }, "operation-statement": { "name": "meta.operation-statement.typespec", - "begin": "\\b(op)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", + "begin": "(?:(internal)\\s+)?\\b(op)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", "beginCaptures": { "1": { "name": "keyword.other.tsp" }, "2": { + "name": "keyword.other.tsp" + }, + "3": { "name": "entity.name.function.tsp" } }, @@ -942,12 +966,15 @@ }, "scalar-statement": { "name": "meta.scalar-statement.typespec", - "begin": "\\b(scalar)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", + "begin": "(?:(internal)\\s+)?\\b(scalar)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", "beginCaptures": { "1": { "name": "keyword.other.tsp" }, "2": { + "name": "keyword.other.tsp" + }, + "3": { "name": "entity.name.type.tsp" } }, @@ -1353,12 +1380,15 @@ }, "union-statement": { "name": "meta.union-statement.typespec", - "begin": "\\b(union)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", + "begin": "(?:(internal)\\s+)?\\b(union)\\b\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)", "beginCaptures": { "1": { "name": "keyword.other.tsp" }, "2": { + "name": "keyword.other.tsp" + }, + "3": { "name": "entity.name.type.tsp" } }, diff --git a/packages/compiler/src/server/tmlanguage.ts b/packages/compiler/src/server/tmlanguage.ts index 373cf2dadf6..7ee817ef45a 100644 --- a/packages/compiler/src/server/tmlanguage.ts +++ b/packages/compiler/src/server/tmlanguage.ts @@ -549,9 +549,10 @@ const modelHeritage: BeginEndRule = { const modelStatement: BeginEndRule = { key: "model-statement", scope: meta, - begin: "\\b(model)\\b", + begin: "(?:(internal)\\s+)?\\b(model)\\b", beginCaptures: { "1": { scope: "keyword.other.tsp" }, + "2": { scope: "keyword.other.tsp" }, }, end: `(?<=\\})|${universalEnd}`, patterns: [ @@ -616,10 +617,11 @@ const scalarBody: BeginEndRule = { const scalarStatement: BeginEndRule = { key: "scalar-statement", scope: meta, - begin: `\\b(scalar)\\b\\s+(${identifier})`, + begin: `(?:(internal)\\s+)?\\b(scalar)\\b\\s+(${identifier})`, beginCaptures: { "1": { scope: "keyword.other.tsp" }, - "2": { scope: "entity.name.type.tsp" }, + "2": { scope: "keyword.other.tsp" }, + "3": { scope: "entity.name.type.tsp" }, }, end: `(?<=\\})|${universalEnd}`, patterns: [ @@ -659,10 +661,11 @@ const enumBody: BeginEndRule = { const enumStatement: BeginEndRule = { key: "enum-statement", scope: meta, - begin: `\\b(enum)\\b\\s+(${identifier})`, + begin: `(?:(internal)\\s+)?\\b(enum)\\b\\s+(${identifier})`, beginCaptures: { "1": { scope: "keyword.other.tsp" }, - "2": { scope: "entity.name.type.tsp" }, + "2": { scope: "keyword.other.tsp" }, + "3": { scope: "entity.name.type.tsp" }, }, end: `(?<=\\})|${universalEnd}`, patterns: [token, enumBody], @@ -697,10 +700,11 @@ const unionBody: BeginEndRule = { const unionStatement: BeginEndRule = { key: "union-statement", scope: meta, - begin: `\\b(union)\\b\\s+(${identifier})`, + begin: `(?:(internal)\\s+)?\\b(union)\\b\\s+(${identifier})`, beginCaptures: { "1": { scope: "keyword.other.tsp" }, - "2": { scope: "entity.name.type.tsp" }, + "2": { scope: "keyword.other.tsp" }, + "3": { scope: "entity.name.type.tsp" }, }, end: `(?<=\\})|${universalEnd}`, patterns: [token, unionBody], @@ -720,10 +724,11 @@ const aliasAssignment: BeginEndRule = { const aliasStatement: BeginEndRule = { key: "alias-statement", scope: meta, - begin: `\\b(alias)\\b\\s+(${identifier})\\s*`, + begin: `(?:(internal)\\s+)?\\b(alias)\\b\\s+(${identifier})\\s*`, beginCaptures: { "1": { scope: "keyword.other.tsp" }, - "2": { scope: "entity.name.type.tsp" }, + "2": { scope: "keyword.other.tsp" }, + "3": { scope: "entity.name.type.tsp" }, }, end: universalEnd, patterns: [aliasAssignment, typeParameters], @@ -732,10 +737,11 @@ const aliasStatement: BeginEndRule = { const constStatement: BeginEndRule = { key: "const-statement", scope: meta, - begin: `\\b(const)\\b\\s+(${identifier})`, + begin: `(?:(internal)\\s+)?\\b(const)\\b\\s+(${identifier})`, beginCaptures: { "1": { scope: "keyword.other.tsp" }, - "2": { scope: "variable.name.tsp" }, + "2": { scope: "keyword.other.tsp" }, + "3": { scope: "variable.name.tsp" }, }, end: universalEnd, patterns: [typeAnnotation, operatorAssignment, expression], @@ -798,10 +804,11 @@ const operationSignature: IncludeRule = { const operationStatement: BeginEndRule = { key: "operation-statement", scope: meta, - begin: `\\b(op)\\b\\s+(${identifier})`, + begin: `(?:(internal)\\s+)?\\b(op)\\b\\s+(${identifier})`, beginCaptures: { "1": { scope: "keyword.other.tsp" }, - "2": { scope: "entity.name.function.tsp" }, + "2": { scope: "keyword.other.tsp" }, + "3": { scope: "entity.name.function.tsp" }, }, end: universalEnd, patterns: [token, operationSignature], @@ -847,9 +854,10 @@ const interfaceBody: BeginEndRule = { const interfaceStatement: BeginEndRule = { key: "interface-statement", scope: meta, - begin: "\\b(interface)\\b", + begin: "(?:(internal)\\s+)?\\b(interface)\\b", beginCaptures: { "1": { scope: "keyword.other.tsp" }, + "2": { scope: "keyword.other.tsp" }, }, end: `(?<=\\})|${universalEnd}`, patterns: [ @@ -886,11 +894,12 @@ const usingStatement: BeginEndRule = { const decoratorDeclarationStatement: BeginEndRule = { key: "decorator-declaration-statement", scope: meta, - begin: `(?:(extern)\\s+)?\\b(dec)\\b\\s+(${identifier})`, + begin: `(?:(internal)\\s+)?(?:(extern)\\s+)?\\b(dec)\\b\\s+(${identifier})`, beginCaptures: { "1": { scope: "keyword.other.tsp" }, "2": { scope: "keyword.other.tsp" }, - "3": { scope: "entity.name.function.tsp" }, + "3": { scope: "keyword.other.tsp" }, + "4": { scope: "entity.name.function.tsp" }, }, end: universalEnd, patterns: [token, operationParameters], @@ -899,11 +908,12 @@ const decoratorDeclarationStatement: BeginEndRule = { const functionDeclarationStatement: BeginEndRule = { key: "function-declaration-statement", scope: meta, - begin: `(?:(extern)\\s+)?\\b(fn)\\b\\s+(${identifier})`, + begin: `(?:(internal)\\s+)?(?:(extern)\\s+)?\\b(fn)\\b\\s+(${identifier})`, beginCaptures: { "1": { scope: "keyword.other.tsp" }, "2": { scope: "keyword.other.tsp" }, - "3": { scope: "entity.name.function.tsp" }, + "3": { scope: "keyword.other.tsp" }, + "4": { scope: "entity.name.function.tsp" }, }, end: universalEnd, patterns: [token, operationParameters, typeAnnotation], diff --git a/website/src/content/docs/docs/language-basics/access-modifiers.md b/website/src/content/docs/docs/language-basics/access-modifiers.md index 16e1762fcf9..5b04523219d 100644 --- a/website/src/content/docs/docs/language-basics/access-modifiers.md +++ b/website/src/content/docs/docs/language-basics/access-modifiers.md @@ -30,8 +30,8 @@ model MyPublicModel { The `internal` modifier can be applied to the following declaration types: -| Declaration | Example | -| ----------- | ------------------------------ | +| Declaration | Example | +| ----------- | ---------------------------------- | | `model` | `internal model Example {}` | | `scalar` | `internal scalar Example;` | | `interface` | `internal interface Example {}` | From dae327542437bc18ea7266e2d26bdd8c19c4b6e3 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 20 Feb 2026 15:24:04 -0500 Subject: [PATCH 10/14] Roll locationContext into options --- packages/compiler/src/core/checker.ts | 36 ++++++++++++++------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 250dcd4ae48..d6b3c6066be 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3221,7 +3221,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ctx: CheckContext, node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, options?: Partial | boolean, - locationContext?: LocationContext, ): Sym | undefined { const resolvedOptions: SymbolResolutionOptions = typeof options === "boolean" @@ -3234,9 +3233,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ) { return referenceSymCache.get(node); } - locationContext ??= getLocationContext(program, node); + resolvedOptions.locationContext ??= getLocationContext(program, node); - const sym = resolveTypeReferenceSymInternal(ctx, node, resolvedOptions, locationContext); + const sym = resolveTypeReferenceSymInternal( + ctx, + node, + resolvedOptions as SymbolResolutionOptions & { locationContext: LocationContext }, + ); if (!resolvedOptions.resolveDeclarationOfTemplate) { referenceSymCache.set(node, sym); } @@ -3246,8 +3249,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function resolveTypeReferenceSymInternal( ctx: CheckContext, node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, - options: SymbolResolutionOptions, - locationContext: LocationContext, + options: SymbolResolutionOptions & { locationContext: LocationContext }, ): Sym | undefined { if (hasParseError(node)) { // Don't report synthetic identifiers used for parser error recovery. @@ -3255,7 +3257,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return undefined; } if (node.kind === SyntaxKind.TypeReference) { - return resolveTypeReferenceSym(ctx, node.target, options, locationContext); + return resolveTypeReferenceSym(ctx, node.target, options); } else if (node.kind === SyntaxKind.Identifier) { const links = resolver.getNodeLinks(node); if (ctx.mapper === undefined && links.resolutionResult) { @@ -3279,19 +3281,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const sym = links.resolvedSymbol?.symbolSource ?? links.resolvedSymbol; - checkSymbolAccess(locationContext, node, sym); + checkSymbolAccess(options.locationContext, node, sym); return sym; } else if (node.kind === SyntaxKind.MemberExpression) { - let base = resolveTypeReferenceSym( - ctx, - node.base, - { - ...options, - resolveDecorators: false, // when resolving decorator the base cannot also be one - }, - locationContext, - ); + let base = resolveTypeReferenceSym(ctx, node.base, { + ...options, + resolveDecorators: false, // when resolving decorator the base cannot also be one + }); if (!base) { return undefined; @@ -3350,7 +3347,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } const sym = resolveMemberInContainer(base, node, options); - checkSymbolAccess(locationContext, node, sym); + checkSymbolAccess(options.locationContext, node, sym); return sym; } @@ -7064,6 +7061,11 @@ interface SymbolResolutionOptions { * @default false */ resolveDeclarationOfTemplate: boolean; + + /** + * Location context to use when resolving the symbol. This is used to enforce symbol access logic. + */ + locationContext?: LocationContext; } const defaultSymbolResolutionOptions: SymbolResolutionOptions = { From c36f5219faf6c0be879f9711d68546fed84c6b91 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 20 Feb 2026 15:37:51 -0500 Subject: [PATCH 11/14] chronus --- .../witemple-msft-internal-symbols-2026-1-20-15-37-30.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/witemple-msft-internal-symbols-2026-1-20-15-37-30.md diff --git a/.chronus/changes/witemple-msft-internal-symbols-2026-1-20-15-37-30.md b/.chronus/changes/witemple-msft-internal-symbols-2026-1-20-15-37-30.md new file mode 100644 index 00000000000..875004d80f6 --- /dev/null +++ b/.chronus/changes/witemple-msft-internal-symbols-2026-1-20-15-37-30.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Added experimental support for `internal` modifiers on type declarations. Any type _except `namespace`_ can be declared `internal`. An `internal` symbol can only be accessed from within the same package where it was declared. From 5f8aacfcf689efaa7b6f3a5a07a8d824f77e11fc Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 20 Feb 2026 15:38:55 -0500 Subject: [PATCH 12/14] lint --- packages/compiler/src/core/binder.ts | 2 -- packages/compiler/src/core/modifiers.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/compiler/src/core/binder.ts b/packages/compiler/src/core/binder.ts index 8b005abaf56..4dae6e82407 100644 --- a/packages/compiler/src/core/binder.ts +++ b/packages/compiler/src/core/binder.ts @@ -383,8 +383,6 @@ export function createBinder(program: Program): Binder { } function bindModelStatement(node: ModelStatementNode) { - if (node.modifierFlags & ModifierFlags.Internal) debugger; - const internal = node.modifierFlags & ModifierFlags.Internal ? SymbolFlags.Internal : SymbolFlags.None; diff --git a/packages/compiler/src/core/modifiers.ts b/packages/compiler/src/core/modifiers.ts index 05966b4ac4e..dd93fa8ea73 100644 --- a/packages/compiler/src/core/modifiers.ts +++ b/packages/compiler/src/core/modifiers.ts @@ -66,7 +66,7 @@ export function checkModifiers(program: Program, node: Declaration): boolean { // Emit experimental warning for any use of the 'internal' modifier. if (node.modifierFlags & ModifierFlags.Internal) { const internalModifiers = filterModifiersByFlags(node.modifiers, ModifierFlags.Internal); - for (const modifier of internalModifiers) { + for (const _ of internalModifiers) { program.reportDiagnostic( createDiagnostic({ code: "experimental-feature", From 5f8d6a57a8268944fcfd741130fa1075d7846e40 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 20 Feb 2026 16:29:10 -0500 Subject: [PATCH 13/14] Fix formatter escaping around items named 'internal' and 'extern' --- .../compiler/src/core/helpers/syntax-utils.ts | 14 ++++++++--- .../test/core/helpers/syntax-utils.test.ts | 18 ++++++++++++- .../compiler/test/formatter/formatter.test.ts | 25 +++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/core/helpers/syntax-utils.ts b/packages/compiler/src/core/helpers/syntax-utils.ts index 6b7346b2fe3..3421cac5e8e 100644 --- a/packages/compiler/src/core/helpers/syntax-utils.ts +++ b/packages/compiler/src/core/helpers/syntax-utils.ts @@ -1,5 +1,5 @@ import { CharCode, isIdentifierContinue, isIdentifierStart, utf16CodeUnits } from "../charcode.js"; -import { Keywords, ReservedKeywords } from "../scanner.js"; +import { isModifier, Keywords, ReservedKeywords } from "../scanner.js"; import { IdentifierNode, MemberExpressionNode, SyntaxKind, TypeReferenceNode } from "../types.js"; /** @@ -34,8 +34,16 @@ function needBacktick(sv: string, context: "allow-reserved" | "disallow-reserved if (sv.length === 0) { return false; } - if (context === "allow-reserved" && ReservedKeywords.has(sv)) { - return false; + if (context === "allow-reserved") { + if (ReservedKeywords.has(sv)) { + return false; + } + // Modifier keywords (e.g. "internal", "extern") are contextual and can be + // used as identifiers without escaping in non-modifier positions. + const kwToken = Keywords.get(sv); + if (kwToken !== undefined && isModifier(kwToken)) { + return false; + } } if (Keywords.has(sv)) { return true; diff --git a/packages/compiler/test/core/helpers/syntax-utils.test.ts b/packages/compiler/test/core/helpers/syntax-utils.test.ts index 540e2b01688..8240d9b01bf 100644 --- a/packages/compiler/test/core/helpers/syntax-utils.test.ts +++ b/packages/compiler/test/core/helpers/syntax-utils.test.ts @@ -1,5 +1,5 @@ import { expect, it } from "vitest"; -import { printIdentifier } from "../../../src/index.js"; +import { printIdentifier } from "../../../src/core/helpers/syntax-utils.js"; it.each([ ["foo", "foo"], @@ -10,3 +10,19 @@ it.each([ ])("%s -> %s", (a, b) => { expect(printIdentifier(a)).toEqual(b); }); + +// Modifier keywords require backtick escaping in default (disallow-reserved) context +it.each([ + ["internal", "`internal`"], + ["extern", "`extern`"], +])("%s -> %s (disallow-reserved)", (a, b) => { + expect(printIdentifier(a)).toEqual(b); +}); + +// Modifier keywords do not require backtick escaping in allow-reserved context +it.each([ + ["internal", "internal"], + ["extern", "extern"], +])("%s -> %s (allow-reserved)", (a, b) => { + expect(printIdentifier(a, "allow-reserved")).toEqual(b); +}); diff --git a/packages/compiler/test/formatter/formatter.test.ts b/packages/compiler/test/formatter/formatter.test.ts index 71434be8249..ec0a45a0265 100644 --- a/packages/compiler/test/formatter/formatter.test.ts +++ b/packages/compiler/test/formatter/formatter.test.ts @@ -1794,6 +1794,31 @@ enum Foo { ...Baz, Two: "2", } +`, + }); + }); + + it("does not escape modifier keywords used as enum member names", async () => { + await assertFormat({ + code: ` +enum Foo { internal, extern } + `, + expected: ` +enum Foo { + internal, + extern, +} +`, + }); + }); + + it("does not escape modifier keywords in member expressions", async () => { + await assertFormat({ + code: ` +const x = Foo.internal; +`, + expected: ` +const x = Foo.internal; `, }); }); From c4fa5fe62a2c771d2073b3a9ad5c0727a22e0267 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Tue, 24 Feb 2026 15:05:33 -0500 Subject: [PATCH 14/14] tspd: Ignore internals in doc generation. --- packages/tspd/src/ref-doc/extractor.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/tspd/src/ref-doc/extractor.ts b/packages/tspd/src/ref-doc/extractor.ts index 65de37a3403..98f0e0529cf 100644 --- a/packages/tspd/src/ref-doc/extractor.ts +++ b/packages/tspd/src/ref-doc/extractor.ts @@ -204,9 +204,11 @@ export function extractRefDocs( namespace, { decorator(dec) { + if (hasInternalModifier(dec)) return; collectType(dec, extractDecoratorRefDoc(program, dec), namespaceDoc.decorators); }, operation(operation) { + if (hasInternalModifier(operation)) return; if (!isDeclaredType(operation)) { return; } @@ -220,12 +222,14 @@ export function extractRefDocs( } }, interface(iface) { + if (hasInternalModifier(iface)) return; if (!isDeclaredType(iface)) { return; } collectType(iface, extractInterfaceRefDocs(program, iface), namespaceDoc.interfaces); }, model(model) { + if (hasInternalModifier(model)) return; if (!isDeclaredType(model)) { return; } @@ -235,12 +239,14 @@ export function extractRefDocs( collectType(model, extractModelRefDocs(program, model), namespaceDoc.models); }, enum(e) { + if (hasInternalModifier(e)) return; if (!isDeclaredType(e)) { return; } collectType(e, extractEnumRefDoc(program, e), namespaceDoc.enums); }, union(union) { + if (hasInternalModifier(union)) return; if (!isDeclaredType(union)) { return; } @@ -249,6 +255,7 @@ export function extractRefDocs( } }, scalar(scalar) { + if (hasInternalModifier(scalar)) return; collectType(scalar, extractScalarRefDocs(program, scalar), namespaceDoc.scalars); }, }, @@ -277,6 +284,14 @@ export function extractRefDocs( }); } +/** Check if a type's declaration has the `internal` modifier. */ +function hasInternalModifier(type: Type): boolean { + const node = type.node; + if (node === undefined) return false; + if (!("modifiers" in node)) return false; + return node.modifiers.some((m: any) => m.kind === SyntaxKind.InternalKeyword); +} + function extractTemplateParameterDocs(program: Program, type: TemplatedType) { if (isTemplateDeclaration(type)) { const templateParamsDocs = getTemplateParameterDocs(type);