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. diff --git a/grammars/typespec.json b/grammars/typespec.json index f5d847fc0c7..ac86e13aa0c 100644 --- a/grammars/typespec.json +++ b/grammars/typespec.json @@ -19,7 +19,7 @@ "name": "keyword.operator.assignment.tsp" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#expression" @@ -28,16 +28,19 @@ }, "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" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#alias-id" @@ -58,7 +61,7 @@ "name": "entity.name.tag.tsp" } }, - "end": "(?=([_$[:alpha:]]|`))|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=([_$[:alpha:]]|`))|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -108,16 +111,19 @@ }, "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" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#type-annotation" @@ -141,7 +147,7 @@ "name": "entity.name.tag.tsp" } }, - "end": "(?=([_$[:alpha:]]|`))|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=([_$[:alpha:]]|`))|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -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,10 +168,13 @@ "name": "keyword.other.tsp" }, "3": { + "name": "keyword.other.tsp" + }, + "4": { "name": "entity.name.function.tsp" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -186,7 +195,7 @@ "name": "keyword.directive.name.tsp" } }, - "end": "$|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "$|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#string-literal" @@ -311,7 +320,7 @@ "name": "keyword.operator.type.annotation.tsp" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -323,16 +332,19 @@ }, "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" } }, - "end": "(?<=\\})|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?<=\\})|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -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,10 +409,13 @@ "name": "keyword.other.tsp" }, "3": { + "name": "keyword.other.tsp" + }, + "4": { "name": "entity.name.function.tsp" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -425,7 +440,7 @@ "name": "keyword.other.tsp" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -472,7 +487,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" @@ -493,7 +508,7 @@ "name": "entity.name.function.tsp" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -505,13 +520,16 @@ }, "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)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?<=\\})|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -577,7 +595,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" @@ -598,7 +616,7 @@ "name": "string.quoted.double.tsp" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -616,13 +634,16 @@ }, "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)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?<=\\})|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -661,7 +682,7 @@ "namespace-name": { "name": "meta.namespace-name.typespec", "begin": "(?=([_$[:alpha:]]|`))", - "end": "((?=\\{)|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b))", + "end": "((?=\\{)|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b))", "patterns": [ { "include": "#identifier-expression" @@ -679,7 +700,7 @@ "name": "keyword.other.tsp" } }, - "end": "((?<=\\})|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b))", + "end": "((?<=\\})|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b))", "patterns": [ { "include": "#token" @@ -739,7 +760,7 @@ "name": "keyword.operator.type.annotation.tsp" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -757,7 +778,7 @@ "name": "keyword.other.tsp" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#expression" @@ -814,16 +835,19 @@ }, "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" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -912,7 +936,7 @@ "name": "entity.name.function.tsp" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -930,7 +954,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" @@ -942,16 +966,19 @@ }, "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" } }, - "end": "(?<=\\})|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?<=\\})|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -975,7 +1002,7 @@ "name": "keyword.operator.spread.tsp" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#expression" @@ -1183,7 +1210,7 @@ "name": "keyword.operator.assignment.tsp" } }, - "end": "(?=>)|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=>)|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "endCaptures": { "0": { "name": "keyword.operator.assignment.tsp" @@ -1235,7 +1262,7 @@ "name": "entity.name.type.tsp" } }, - "end": "(?=>)|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=>)|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -1256,7 +1283,7 @@ "name": "keyword.other.tsp" } }, - "end": "(?=>)|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=>)|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#expression" @@ -1271,7 +1298,7 @@ "name": "keyword.operator.assignment.tsp" } }, - "end": "(?=>)|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=>)|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#expression" @@ -1309,7 +1336,7 @@ "name": "keyword.other.tsp" } }, - "end": "(?=>)|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=>)|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#expression" @@ -1353,16 +1380,19 @@ }, "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" } }, - "end": "(?<=\\})|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?<=\\})|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -1383,7 +1413,7 @@ "name": "keyword.operator.type.annotation.tsp" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -1401,7 +1431,7 @@ "name": "keyword.other.tsp" } }, - "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern|internal)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", "patterns": [ { "include": "#token" @@ -1422,7 +1452,7 @@ "name": "keyword.other.tsp" } }, - "end": "(?=>)|(?=,|;|@|#[a-z]|\\)|\\}|\\b(?:extern)\\b|\\b(?:namespace|model|op|using|import|enum|alias|union|interface|dec|fn)\\b)", + "end": "(?=>)|(?=,|;|@|#[a-z]|\\)|\\}|\\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 1e7b10c3c65..4dae6e82407 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, @@ -380,7 +383,10 @@ export function createBinder(program: Program): Binder { } function bindModelStatement(node: ModelStatementNode) { - declareSymbol(node, SymbolFlags.Model | SymbolFlags.Declaration); + 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(); } @@ -398,7 +404,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(); } @@ -408,26 +416,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) { @@ -473,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) { @@ -510,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: @@ -533,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) @@ -547,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 = scope; if ( flags & SymbolFlags.Namespace && diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 412963497fb..d6b3c6066be 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -12,11 +12,13 @@ import { import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; import { compilerAssert, ignoreDiagnostics, reportDeprecated } 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"; 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 { @@ -81,6 +83,7 @@ import { JsNamespaceDeclarationNode, LiteralNode, LiteralType, + LocationContext, MemberContainerNode, MemberContainerType, MemberExpressionNode, @@ -94,7 +97,6 @@ import { ModelProperty, ModelPropertyNode, ModelStatementNode, - ModifierFlags, Namespace, NamespaceStatementNode, NeverType, @@ -2056,6 +2058,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker // 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( @@ -2064,10 +2067,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ); const name = node.id.sv; - if (!(node.modifierFlags & ModifierFlags.Extern)) { - reportCheckerDiagnostic(createDiagnostic({ code: "decorator-extern", target: node })); - } - const implementation = symbol.value; if (implementation === undefined) { reportCheckerDiagnostic(createDiagnostic({ code: "missing-implementation", target: node })); @@ -2090,6 +2089,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } function checkFunctionDeclaration(ctx: CheckContext, node: FunctionDeclarationStatementNode) { + checkModifiers(program, node); reportCheckerDiagnostic(createDiagnostic({ code: "function-unsupported", target: node })); return errorType; } @@ -2313,6 +2313,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(ctx, x)); } else if (node.statements) { @@ -2462,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( @@ -3229,7 +3233,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ) { return referenceSymCache.get(node); } - const sym = resolveTypeReferenceSymInternal(ctx, node, resolvedOptions); + resolvedOptions.locationContext ??= getLocationContext(program, node); + + const sym = resolveTypeReferenceSymInternal( + ctx, + node, + resolvedOptions as SymbolResolutionOptions & { locationContext: LocationContext }, + ); if (!resolvedOptions.resolveDeclarationOfTemplate) { referenceSymCache.set(node, sym); } @@ -3239,7 +3249,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function resolveTypeReferenceSymInternal( ctx: CheckContext, node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, - options: SymbolResolutionOptions, + options: SymbolResolutionOptions & { locationContext: LocationContext }, ): Sym | undefined { if (hasParseError(node)) { // Don't report synthetic identifiers used for parser error recovery. @@ -3269,8 +3279,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } } - const sym = links.resolvedSymbol; - return sym?.symbolSource ?? sym; + const sym = links.resolvedSymbol?.symbolSource ?? links.resolvedSymbol; + + checkSymbolAccess(options.locationContext, node, sym); + + return sym; } else if (node.kind === SyntaxKind.MemberExpression) { let base = resolveTypeReferenceSym(ctx, node.base, { ...options, @@ -3332,12 +3345,60 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } base = baseSym; } - return resolveMemberInContainer(base, node, options); + const sym = resolveMemberInContainer(base, node, options); + + checkSymbolAccess(options.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; + + 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 }), @@ -3801,6 +3862,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[] = []; @@ -5460,6 +5524,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[] = []; @@ -5608,6 +5675,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); @@ -5647,6 +5717,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker if (links.value !== undefined) { return links.value; } + checkModifiers(program, node); const type = node.type ? getTypeForNode(node.type, undefined) : undefined; @@ -5694,6 +5765,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkEnum(ctx: CheckContext, node: EnumStatementNode): Type { const links = getSymbolLinks(node.symbol); if (!links.type) { + checkModifiers(program, node); const enumType: Enum = (links.type = createType({ kind: "Enum", name: node.id.sv, @@ -5757,6 +5829,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({ @@ -5871,6 +5946,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(); @@ -6983,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 = { 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/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 5c42d275d59..43e967aefc1 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": { @@ -527,16 +528,25 @@ const diagnostics = { default: "A rest parameter must be of an array type.", }, }, - "decorator-extern": { + "function-extern": { severity: "error", messages: { - default: "A decorator declaration must be prefixed with the 'extern' modifier.", + default: "A function declaration must be prefixed with the 'extern' modifier.", }, }, - "function-extern": { + "invalid-modifier": { severity: "error", messages: { - default: "A function declaration must be prefixed with the 'extern' modifier.", + 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"}'.`, + }, + }, + "experimental-feature": { + severity: "warning", + messages: { + 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 new file mode 100644 index 00000000000..dd93fa8ea73 --- /dev/null +++ b/packages/compiler/src/core/modifiers.ts @@ -0,0 +1,202 @@ +// 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, Modifier, 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 NO_MODIFIERS: ModifierCompatibility = { + allowed: ModifierFlags.None, + required: ModifierFlags.None, +}; + +const SYNTAX_MODIFIERS: Readonly> = { + [SyntaxKind.NamespaceStatement]: NO_MODIFIERS, + [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; + + // Emit experimental warning for any use of the 'internal' modifier. + if (node.modifierFlags & ModifierFlags.Internal) { + const internalModifiers = filterModifiersByFlags(node.modifiers, ModifierFlags.Internal); + for (const _ of internalModifiers) { + program.reportDiagnostic( + createDiagnostic({ + code: "experimental-feature", + messageId: "internal", + target: node, + }), + ); + } + } + + 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; + + 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, + }), + ); + } + } + + const missingRequiredModifiers = compatibility.required & ~node.modifierFlags; + + if (missingRequiredModifiers) { + // There is at least one required modifier missing from this syntax node. + isValid = false; + + 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/name-resolver.ts b/packages/compiler/src/core/name-resolver.ts index ee11f573729..53792d19c75 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, @@ -1212,6 +1213,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 e977aa02fe2..4045c34195d 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -2,10 +2,12 @@ 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, isKeyword, + isModifier, isPunctuation, isReservedKeyword, isStatementKeyword, @@ -27,6 +29,7 @@ import { CallExpressionNode, Comment, ConstStatementNode, + Declaration, DeclarationNode, DecoratorDeclarationStatementNode, DecoratorExpressionNode, @@ -57,6 +60,7 @@ import { IdentifierNode, ImportStatementNode, InterfaceStatementNode, + InternalKeywordNode, InvalidStatementNode, LineComment, MemberExpressionNode, @@ -435,35 +439,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 +474,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 +541,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 +571,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 +609,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseNamespaceStatement( pos: number, decorators: DecoratorExpressionNode[], + modifiers: Modifier[], docs: DocNode[], directives: DirectiveExpressionNode[], ): NamespaceStatementNode { @@ -645,6 +638,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa locals: undefined!, statements, directives: directives, + modifiers, + modifierFlags: modifiersToFlags(modifiers), ...finishNode(pos), }; @@ -656,6 +651,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa id: nsSegments[i], statements: outerNs, locals: undefined!, + modifiers: [], + modifierFlags: ModifierFlags.None, ...finishNode(pos), }; } @@ -666,6 +663,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 +681,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 +694,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa bodyRange, extends: extendList.items, decorators, + modifiers, + modifierFlags: modifiersToFlags(modifiers), ...finishNode(pos), }; } @@ -719,6 +720,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 +735,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa templateParameters, templateParametersRange, decorators, + modifiers, + modifierFlags: modifiersToFlags(modifiers), options, ...finishNode(pos), }; @@ -742,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, @@ -820,11 +828,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 +896,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa templateParametersRange, signature, decorators, + modifiers, + modifierFlags: modifiersToFlags(modifiers), ...finishNode(pos), }; } @@ -894,6 +920,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 +956,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa decorators, properties: propDetail.items, bodyRange: propDetail.range, + modifiers, + modifierFlags: modifiersToFlags(modifiers), ...finishNode(pos), }; } @@ -1096,6 +1125,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 +1144,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa members, bodyRange, decorators, + modifiers, + modifierFlags: modifiersToFlags(modifiers), ...finishNode(pos), }; } @@ -1154,6 +1186,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 +1195,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa kind: SyntaxKind.EnumStatement, id, decorators, + modifiers, + modifierFlags: modifiersToFlags(modifiers), members, ...finishNode(pos), }; @@ -1222,7 +1257,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 +1271,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 +1289,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa id, value, type, + modifiers, + modifierFlags: modifiersToFlags(modifiers), ...finishNode(pos), }; } @@ -1716,6 +1755,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); @@ -1956,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() } }); @@ -1985,9 +2035,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 +2082,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa switch (token()) { case Token.ExternKeyword: return parseExternKeyword(); + case Token.InternalKeyword: + return parseInternalKeyword(); default: return undefined; } @@ -2120,18 +2195,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; - } - } - return flags; - } - function parseRange(mode: ParseMode, range: TextRange, callback: () => T): T { const savedMode = currentMode; const result = scanner.scanRange(range, () => { @@ -3080,6 +3143,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/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/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 1a8cb3b9a4c..01542a49235 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -962,6 +962,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) @@ -1066,6 +1071,7 @@ export enum SyntaxKind { ConstStatement, CallExpression, ScalarConstructor, + InternalKeyword, } export const enum NodeFlags { @@ -1246,8 +1252,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 @@ -1280,22 +1287,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 @@ -1622,6 +1630,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; } @@ -1666,19 +1678,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 @@ -1689,8 +1705,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. */ @@ -1728,8 +1742,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 fe9834ce1c4..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, @@ -249,6 +250,8 @@ export function printNode( ); case SyntaxKind.ExternKeyword: return "extern"; + case SyntaxKind.InternalKeyword: + return "internal"; case SyntaxKind.VoidKeyword: return "void"; case SyntaxKind.NeverKeyword: @@ -333,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( @@ -344,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( @@ -661,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( @@ -708,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( @@ -750,6 +784,7 @@ export function printInterfaceStatement( return [ decorators, + printModifiers(path, options, print), "interface ", id, generic, @@ -1011,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, @@ -1199,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, @@ -1296,6 +1333,7 @@ export function printOperationStatement( return [ decorators, + printModifiers(path, options, print), inInterface ? "" : "op ", path.call(print, "id"), templateParams, @@ -1503,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/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/src/server/tmlanguage.ts b/packages/compiler/src/server/tmlanguage.ts index 6794dc97095..7ee817ef45a 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 = `(?=,|;|@|#[a-z]|\\)|\\}|${modifierKeyword}|${statementKeyword})`; const universalEndExceptComma = `(?=;|@|\\)|\\}|${modifierKeyword}|${statementKeyword})`; @@ -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/packages/compiler/test/binder.test.ts b/packages/compiler/test/binder.test.ts index 488799be5da..82fd490ccd5 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, @@ -565,5 +566,7 @@ function createJsSourceFile(exports: any): JsSourceFileNode { pos: 0, end: 0, flags: NodeFlags.None, + modifiers: [], + modifierFlags: ModifierFlags.None, }; } diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index bc5c2dd0e1a..46706700257 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -118,8 +118,8 @@ describe("compiler: checker: decorators", () => { dec testDec(target: unknown); `); expectDiagnostics(diagnostics, { - code: "decorator-extern", - message: "A decorator declaration must be prefixed with the 'extern' modifier.", + code: "invalid-modifier", + message: "Declaration of type 'dec' is missing required modifier 'extern'.", }); }); diff --git a/packages/compiler/test/checker/internal.test.ts b/packages/compiler/test/checker/internal.test.ts new file mode 100644 index 00000000000..3701355f91e --- /dev/null +++ b/packages/compiler/test/checker/internal.test.ts @@ -0,0 +1,367 @@ +import { describe, it } from "vitest"; +import { expectDiagnosticEmpty, expectDiagnostics } from "../../src/testing/index.js"; +import { Tester } from "../tester.js"; + +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-feature", + severity: "warning", + 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.`, + }); + }); + } + + 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-feature", + }); + }); + + it("does not allow 'internal' on namespace", async () => { + const diagnostics = await Tester.diagnose(`internal namespace Foo {}`); + expectDiagnostics(diagnostics, [ + { + code: "experimental-feature", + }, + { + 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-feature", + }, + { + 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", () => { + 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-feature" }, + ]); + }); + + 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-feature" }, + ]); + }); + + 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-feature" }, + ]); + }); + + 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-feature" }, + ]); + }); + + 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-feature" }, + ]); + }); + + 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-feature" }, + ]); + }); + + 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-feature" }, + ]); + }); + + 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-feature" }, + ]); + }); + + 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-feature" }, + ]); + }); + + 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-feature" }, + ]); + }); + }); + + 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-feature" }); + }); + + 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-feature" }); + }); + + 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-feature" }); + }); + + 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-feature" }); + }); + + 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-feature" }); + }); + + 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-feature" }); + }); + }); + + 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": ` + internal model InternalHelper {} + `, + }).diagnose(` + import "my-lib"; + model Consumer { x: Public } + `); + + // experimental-feature for InternalHelper in the library, no access error + expectDiagnostics(diagnostics, { code: "experimental-feature" }); + }); + }); + + 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 {} + + model PublicModel { prop: InternalModel; } + `, + }).diagnose(` + import "my-lib"; + model Consumer { x: PublicModel } + `); + + expectDiagnostics(diagnostics, { code: "experimental-feature" }); + }); + + 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(` + import "my-lib"; + model Consumer { x: MyLib.PublicModel } + `); + + expectDiagnostics(diagnostics, { code: "experimental-feature" }); + }); + }); +}); + +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/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; `, }); }); 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, }; } 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";']); 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, ]; 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"]; 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); 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..5b04523219d --- /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.