Bug Report
🔎 Search Terms
semantic highlighting, semantic token, PrivateIdentifier, private field, private method, encodedSemanticClassifications
🕗 Version & Regression Information
- TypeScript 6.0.3 (bundled with VS Code)
- Also present in the v5.9 classifier implementation
- This is not a theme-specific issue
💻 Code
class Foo {
field = 1;
#privateField = 1;
method() {}
#privateMethod() {}
test() {
this.field;
this.#privateField;
this.method();
this.#privateMethod();
}
}
🙁 Actual behavior
In VS Code's Developer: Inspect Editor Tokens and Scopes:
field and method receive semantic token types.
#privateField and #privateMethod receive no semantic token and fall back to TextMate scopes.
- As a result,
editor.semanticTokenColorCustomizations rules for property and method do not apply to ECMAScript private members.
🙂 Expected behavior
#privateField should be classified as property.
#privateMethod should be classified as method.
- Declarations should also receive the
declaration modifier.
Root cause
VS Code requests encodedSemanticClassifications-full with format "2020".
In src/services/classifier2020.ts, collectTokens only enters the semantic-classification path for isIdentifier(node):
if (isIdentifier(node) && ...) {
let symbol = typeChecker.getSymbolAtLocation(node);
// ...
}
ECMAScript private names are separate SyntaxKind.PrivateIdentifier nodes, so they are skipped before getSymbolAtLocation is called.
Relevant source:
|
|
|
function collectTokens(program: Program, sourceFile: SourceFile, span: TextSpan, collector: (node: Node, tokenType: number, tokenModifier: number) => void, cancellationToken: CancellationToken) { |
|
const typeChecker = program.getTypeChecker(); |
|
|
|
let inJSXElement = false; |
|
|
|
function visit(node: Node) { |
|
switch (node.kind) { |
|
case SyntaxKind.ModuleDeclaration: |
|
case SyntaxKind.ClassDeclaration: |
|
case SyntaxKind.InterfaceDeclaration: |
|
case SyntaxKind.FunctionDeclaration: |
|
case SyntaxKind.ClassExpression: |
|
case SyntaxKind.FunctionExpression: |
|
case SyntaxKind.ArrowFunction: |
|
cancellationToken.throwIfCancellationRequested(); |
|
} |
|
|
|
if (!node || !textSpanIntersectsWith(span, node.pos, node.getFullWidth()) || node.getFullWidth() === 0) { |
|
return; |
|
} |
|
const prevInJSXElement = inJSXElement; |
|
if (isJsxElement(node) || isJsxSelfClosingElement(node)) { |
|
inJSXElement = true; |
|
} |
|
if (isJsxExpression(node)) { |
|
inJSXElement = false; |
|
} |
|
|
|
if (isIdentifier(node) && !inJSXElement && !inImportClause(node) && !isInfinityOrNaNString(node.escapedText)) { |
|
let symbol = typeChecker.getSymbolAtLocation(node); |
|
if (symbol) { |
|
if (symbol.flags & SymbolFlags.Alias) { |
|
symbol = typeChecker.getAliasedSymbol(symbol); |
|
} |
|
let typeIdx = classifySymbol(symbol, getMeaningFromLocation(node)); |
|
if (typeIdx !== undefined) { |
|
let modifierSet = 0; |
|
if (node.parent) { |
|
const parentIsDeclaration = isBindingElement(node.parent) || tokenFromDeclarationMapping.get(node.parent.kind) === typeIdx; |
|
if (parentIsDeclaration && (node.parent as NamedDeclaration).name === node) { |
|
modifierSet = 1 << TokenModifier.declaration; |
|
} |
|
} |
|
|
|
// property declaration in constructor |
|
if (typeIdx === TokenType.parameter && isRightSideOfQualifiedNameOrPropertyAccess(node)) { |
|
typeIdx = TokenType.property; |
|
} |
|
|
|
typeIdx = reclassifyByType(typeChecker, node, typeIdx); |
|
|
|
const decl = symbol.valueDeclaration; |
|
if (decl) { |
|
const modifiers = getCombinedModifierFlags(decl); |
|
const nodeFlags = getCombinedNodeFlags(decl); |
|
if (modifiers & ModifierFlags.Static) { |
|
modifierSet |= 1 << TokenModifier.static; |
|
} |
|
if (modifiers & ModifierFlags.Async) { |
|
modifierSet |= 1 << TokenModifier.async; |
|
} |
|
if (typeIdx !== TokenType.class && typeIdx !== TokenType.interface) { |
|
if ((modifiers & ModifierFlags.Readonly) || (nodeFlags & NodeFlags.Const) || (symbol.getFlags() & SymbolFlags.EnumMember)) { |
|
modifierSet |= 1 << TokenModifier.readonly; |
|
} |
|
} |
|
if ((typeIdx === TokenType.variable || typeIdx === TokenType.function) && isLocalDeclaration(decl, sourceFile)) { |
|
modifierSet |= 1 << TokenModifier.local; |
|
} |
|
if (program.isSourceFileDefaultLibrary(decl.getSourceFile())) { |
|
modifierSet |= 1 << TokenModifier.defaultLibrary; |
|
} |
|
} |
|
else if (symbol.declarations && symbol.declarations.some(d => program.isSourceFileDefaultLibrary(d.getSourceFile()))) { |
|
modifierSet |= 1 << TokenModifier.defaultLibrary; |
|
} |
|
|
|
collector(node, typeIdx, modifierSet); |
|
} |
|
} |
|
} |
|
forEachChild(node, visit); |
Proposed fix
+ isPrivateIdentifier,
isPropertyAccessExpression,
- if (isIdentifier(node) && ...) {
+ if ((isIdentifier(node) || isPrivateIdentifier(node)) && ...) {
The existing declaration-kind mapping already classifies private fields and methods correctly once these nodes reach the symbol-classification path.
I tested the equivalent change against TypeScript 6.0.3:
#privateField declaration/reference → property
#privateMethod declaration/reference → member internally, mapped by VS Code to method
- declaration occurrences receive the
declaration modifier
- added fourslash regression test passes
Related issue
#44483 requests new private/protected semantic modifiers. This report is different: ECMAScript PrivateIdentifier nodes currently receive no semantic classification at all.
Bug Report
🔎 Search Terms
semantic highlighting, semantic token, PrivateIdentifier, private field, private method, encodedSemanticClassifications
🕗 Version & Regression Information
💻 Code
🙁 Actual behavior
In VS Code's Developer: Inspect Editor Tokens and Scopes:
fieldandmethodreceive semantic token types.#privateFieldand#privateMethodreceive no semantic token and fall back to TextMate scopes.editor.semanticTokenColorCustomizationsrules forpropertyandmethoddo not apply to ECMAScript private members.🙂 Expected behavior
#privateFieldshould be classified asproperty.#privateMethodshould be classified asmethod.declarationmodifier.Root cause
VS Code requests
encodedSemanticClassifications-fullwith format"2020".In
src/services/classifier2020.ts,collectTokensonly enters the semantic-classification path forisIdentifier(node):ECMAScript private names are separate
SyntaxKind.PrivateIdentifiernodes, so they are skipped beforegetSymbolAtLocationis called.Relevant source:
TypeScript/src/services/classifier2020.ts
Lines 121 to 203 in 050880c
Proposed fix
+ isPrivateIdentifier, isPropertyAccessExpression,The existing declaration-kind mapping already classifies private fields and methods correctly once these nodes reach the symbol-classification path.
I tested the equivalent change against TypeScript 6.0.3:
#privateFielddeclaration/reference →property#privateMethoddeclaration/reference →memberinternally, mapped by VS Code tomethoddeclarationmodifierRelated issue
#44483 requests new
private/protectedsemantic modifiers. This report is different: ECMAScriptPrivateIdentifiernodes currently receive no semantic classification at all.