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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tools/scripts/tsc/ast.json
Original file line number Diff line number Diff line change
Expand Up @@ -1236,7 +1236,7 @@
"members": [
{
"name": "Text",
"type": "string"
"type": "SymbolName"
}
],
"arena": true
Expand Down
21 changes: 16 additions & 5 deletions tools/scripts/tsc/generate-encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
KindType,
MemberInfo,
NodeType,
Type,
} from "./schema.ts";
import { api } from "./schema.ts";

Expand Down Expand Up @@ -153,7 +154,7 @@ function needsHandWrittenCommonData(node: NodeType): boolean {
if (m.isChild() || m.isKindParam()) return false;
const dt = m.declaredType;
if (dt.kind === "primitive" && dt.name === "bool") return false;
if (dt.kind === "primitive" && dt.name === "string") return false;
if (isStringPrimitive(dt)) return false;
if (isSyntaxKindUnion(m)) return false;
if (isNodeFlagsMember(m)) return false;
return true;
Expand All @@ -170,7 +171,7 @@ function getHandWrittenDataMembers(node: NodeType): MemberInfo[] {
return members.filter(m => {
if (m.isChild() || m.isKindParam()) return false;
const dt = m.declaredType;
if (dt.kind === "primitive" && dt.name === "string") return false;
if (isStringPrimitive(dt)) return false;
if (isNodeFlagsMember(m)) return false;
return true;
});
Expand All @@ -182,12 +183,21 @@ function getHandWrittenDataMembers(node: NodeType): MemberInfo[] {

type DataType = "string" | "children" | "extended";

function isSymbolNamePrimitive(type: Type | undefined): boolean {
return type?.kind === "primitive" && type.name === "SymbolName";
}

// SymbolName is an interned string in Go and is encoded as a string.
function isStringPrimitive(type: Type | undefined): boolean {
return type?.kind === "primitive" && (type.name === "string" || type.name === "SymbolName");
}

function classifyDataType(node: NodeType): DataType {
if (node.handWritten) return "extended";
const members = schemaMembers(node);
const stringMembers = members.filter(m =>
!m.isChild() && !m.isKindParam() &&
m.declaredType.kind === "primitive" && m.declaredType.name === "string"
isStringPrimitive(m.declaredType)
);
if (stringMembers.length > 1) return "extended";
if (stringMembers.length === 1) {
Expand Down Expand Up @@ -292,7 +302,7 @@ function analyzeNode(node: NodeType): EncoderNodeInfo {
const textMember = dataType === "string"
? members.find(m =>
!m.isChild() && !m.isKindParam() &&
m.declaredType.kind === "primitive" && m.declaredType.name === "string"
isStringPrimitive(m.declaredType)
)
: undefined;
// Factory members include all non-noFactory members (even noTS ones),
Expand Down Expand Up @@ -538,10 +548,11 @@ function generateGoRecordNodeStrings(w: CodeWriter) {
const textAccess = info.textMember.private
? `node.${castName}().${api.capitalize(info.textMember.name)}()`
: `node.${castName}().${info.textMember.name}`;
const textValue = isSymbolNamePrimitive(info.textMember.declaredType) ? `${textAccess}.Value()` : textAccess;

w.write(`case ${kinds.join(", ")}:`);
w.push();
w.write(`return strs.add(${textAccess}, node.Kind, node.Pos(), node.End())`);
w.write(`return strs.add(${textValue}, node.Kind, node.Pos(), node.End())`);
w.pop();
}

Expand Down
12 changes: 7 additions & 5 deletions tools/scripts/tsc/generate-enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ function syntaxKindMembers(): EnumMember[] {
return [...members, { name: "Count", value: String(members.length) }, ...api.kindMarkers()];
}

function parseGoConstBlock(block: string, def: EnumDef): EnumMember[] {
function parseGoDeclarationBlock(block: string, def: EnumDef): EnumMember[] {
const prefix = def.goPrefix;
const members: EnumMember[] = [];
let iotaCounter = 0;
Expand Down Expand Up @@ -278,11 +278,12 @@ function translateGoNumericExpression(expression: string, prefix: string): strin
}

/**
* Resolve a Go string-constant expression (e.g. `Prefix + "call"` or `"export="`)
* Resolve a Go string expression, optionally interned with `MakeSymbolName`,
* into a quoted, JS-escaped TypeScript string literal. `replacements` maps bare
* Go identifiers (such as a sentinel-prefix constant) to their literal value.
*/
function parseGoStringValue(goValue: string, replacements: Record<string, string>): string {
goValue = goValue.replace(/^(?:unique\.Make|MakeSymbolName)\((.*)\)$/, "$1");
let result = "";
for (const part of goValue.split("+").map(p => p.trim())) {
if (Object.prototype.hasOwnProperty.call(replacements, part)) {
Expand All @@ -306,10 +307,11 @@ interface EnumMember {

function parseGoEnum(def: EnumDef): EnumMember[] {
const source = fs.readFileSync(path.join(ROOT, def.goFile), "utf-8");
const constBlockRegex = /const\s*\(([\s\S]*?)\n\)/g;
const declarationBlockRegex = /(const|var)\s*\(([\s\S]*?)\n\)/g;

for (const match of source.matchAll(constBlockRegex)) {
const members = parseGoConstBlock(match[1], def).filter(member => !def.excludeMembers?.includes(member.name));
for (const match of source.matchAll(declarationBlockRegex)) {
if (match[1] === "var" && !def.stringEnum) continue;
const members = parseGoDeclarationBlock(match[2], def).filter(member => !def.excludeMembers?.includes(member.name));
if (members.length > 0) return topoSortMembers(members);
}

Expand Down
40 changes: 34 additions & 6 deletions tools/scripts/tsc/generate-go-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function schemaMembers(node: NodeType): MemberInfo[] {

function isTextContentType(type: Type): boolean {
if (type.kind === "primitive") {
return type.name === "string";
return type.name === "string" || type.name === "SymbolName";
}
if (type.kind === "list" && type.listKind === "raw") {
return type.elementType.kind === "primitive" && type.elementType.name === "string";
Expand Down Expand Up @@ -494,11 +494,39 @@ function emitNewFactory(
w.write("");
}

function isSymbolNameMember(m: MemberInfo): boolean {
return m.type.kind === "primitive" && m.type.name === "SymbolName";
}

// Nodes with SymbolName members get a New*FromSymbolName factory taking the
// interned name, plus a New* convenience factory that accepts a string.
function factoryName(name: string, node: NodeType): string {
return schemaMembers(node).some(isSymbolNameMember) ? `New${name}FromSymbolName` : `New${name}`;
}

function emitStringFactory(w: CodeWriter, name: string, node: NodeType, members: MemberInfo[]) {
const params = members.map(m => `${m.goParamName()} ${isSymbolNameMember(m) ? "string" : m.type.formatGoReference()}`).join(", ");
const args = members.map(m => isSymbolNameMember(m) ? `MakeSymbolName(${m.goParamName()})` : m.goParamName()).join(", ");
w.write(`func (f *NodeFactory) New${name}(${params}) *Node {`);
w.push();
w.write(`return f.${factoryName(name, node)}(${args})`);
w.pop();
w.write("}");
w.write("");
}

function generateNewFactory(w: CodeWriter, node: NodeType) {
const structName = node.name;
const members = schemaMembers(node);
const kindMember = members.find(m => m.isKindParam());
const nodeFlagsMembers = members.filter(m => isNodeFlagsMember(m));
const hasSymbolName = members.some(isSymbolNameMember);
const emit = (name: string, kindName: string) => {
if (hasSymbolName) {
emitStringFactory(w, name, node, members);
}
emitNewFactory(w, factoryName(name, node), kindName, structName, node, members, kindMember, nodeFlagsMembers);
};
if (node.name === "Token") {
const tokenKinds = new Set(node.allKinds().map(kind => kind.formatGoConstant()));
w.write("func (f *NodeFactory) NewToken(kind TokenSyntaxKind) *Node {");
Expand Down Expand Up @@ -531,10 +559,10 @@ function generateNewFactory(w: CodeWriter, node: NodeType) {
w.write("");
}
else {
emitNewFactory(w, `New${node.name}`, node.syntaxKindName, structName, node, members, kindMember, nodeFlagsMembers);
emit(node.name, node.syntaxKindName);
}
for (const alias of node.kindAliases) {
emitNewFactory(w, `New${alias}`, alias, structName, node, members, kindMember, nodeFlagsMembers);
emit(alias, alias);
}
}

Expand Down Expand Up @@ -1044,12 +1072,12 @@ function generateClone(w: CodeWriter, node: NodeType) {
w.push();
w.write(`case ${api.kindType(`SyntaxKind.${node.syntaxKindName}`).formatGoConstant()}:`);
w.push();
w.write(`return cloneNode(f.AsNodeFactory().New${node.name}(${args}), node.AsNode(), f.AsNodeFactory().hooks)`);
w.write(`return cloneNode(f.AsNodeFactory().${factoryName(node.name, node)}(${args}), node.AsNode(), f.AsNodeFactory().hooks)`);
w.pop();
for (const alias of node.kindAliases) {
w.write(`case Kind${alias}:`);
w.push();
w.write(`return cloneNode(f.AsNodeFactory().New${alias}(${args}), node.AsNode(), f.AsNodeFactory().hooks)`);
w.write(`return cloneNode(f.AsNodeFactory().${factoryName(alias, node)}(${args}), node.AsNode(), f.AsNodeFactory().hooks)`);
w.pop();
}
w.write("default:");
Expand All @@ -1060,7 +1088,7 @@ function generateClone(w: CodeWriter, node: NodeType) {
w.write("}");
}
else {
w.write(`return cloneNode(f.AsNodeFactory().New${node.name}(${args}), node.AsNode(), f.AsNodeFactory().hooks)`);
w.write(`return cloneNode(f.AsNodeFactory().${factoryName(node.name, node)}(${args}), node.AsNode(), f.AsNodeFactory().hooks)`);
}

w.pop();
Expand Down
8 changes: 8 additions & 0 deletions tools/scripts/tsc/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ test("other TypeScript enums retain their Go sources", () => {
assert.throws(() => generateEnum({ ...def, goFile: "does-not-exist.go" }), /ENOENT/);
});

test("interned internal symbol names retain their escaped string enum values", () => {
const def = enumDefs.find(def => def.name === "InternalSymbolName")!;
const { members, code } = generateEnum(def);
assert.equal(members.length, 20);
const actual = fs.readFileSync(path.join(repoRoot, def.outDir, "internalSymbolName.enum.ts"), "utf8");
assert.equal(actual.replaceAll("\r\n", "\n"), code);
});

test("SyntaxKind is generated without reading its Go definition", () => {
const def = enumDefs.find(def => def.name === "SyntaxKind")!;
const { members, code } = generateEnum({ ...def, goFile: "does-not-exist.go" });
Expand Down
2 changes: 2 additions & 0 deletions tools/scripts/tsc/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export class PrimitiveType extends TypeBase {
const typeMap: Record<string, string> = {
bool: "boolean",
int: "number",
SymbolName: "string",
};
return typeMap[this.name] || this.name;
}
Expand Down Expand Up @@ -804,6 +805,7 @@ export class SchemaAPI {
"ModifierFlags",
"NodeFlags",
"string",
"SymbolName",
"TokenFlags",
]);

Expand Down
12 changes: 6 additions & 6 deletions tsc/internal/api/encoder/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func TestDecodeSourceFile_VariableDeclaration(t *testing.T) {

decl := declList.Declarations.Nodes[0].AsVariableDeclaration()
assert.Equal(t, decl.Name().Kind, ast.KindIdentifier)
assert.Equal(t, decl.Name().AsIdentifier().Text, "x")
assert.Equal(t, decl.Name().AsIdentifier().Text.Value(), "x")
assert.Assert(t, decl.Initializer != nil)
assert.Equal(t, decl.Initializer.Kind, ast.KindNumericLiteral)
assert.Equal(t, decl.Initializer.AsNumericLiteral().Text, "1")
Expand Down Expand Up @@ -165,14 +165,14 @@ func TestDecodeSourceFile_FunctionDeclaration(t *testing.T) {

funcDecl := decoded.Statements.Nodes[0].AsFunctionDeclaration()
assert.Assert(t, funcDecl.Name() != nil)
assert.Equal(t, funcDecl.Name().AsIdentifier().Text, "add")
assert.Equal(t, funcDecl.Name().AsIdentifier().Text.Value(), "add")
assert.Assert(t, funcDecl.Parameters != nil)
assert.Equal(t, len(funcDecl.Parameters.Nodes), 2)
assert.Assert(t, funcDecl.Type != nil)
assert.Assert(t, funcDecl.Body != nil)

param0 := funcDecl.Parameters.Nodes[0].AsParameterDeclaration()
assert.Equal(t, param0.Name().AsIdentifier().Text, "a")
assert.Equal(t, param0.Name().AsIdentifier().Text.Value(), "a")
assert.Assert(t, param0.Type != nil)
}

Expand All @@ -196,7 +196,7 @@ func TestDecodeSourceFile_ImportDeclaration(t *testing.T) {
assert.Assert(t, namedImports.Elements != nil)
assert.Equal(t, len(namedImports.Elements.Nodes), 1)
spec := namedImports.Elements.Nodes[0].AsImportSpecifier()
assert.Equal(t, spec.Name().AsIdentifier().Text, "bar")
assert.Equal(t, spec.Name().AsIdentifier().Text.Value(), "bar")
}

func TestDecodeSourceFile_SourcePhaseImport(t *testing.T) {
Expand Down Expand Up @@ -293,7 +293,7 @@ func TestDecodeSourceFile_ClassDeclaration(t *testing.T) {

classDecl := decoded.Statements.Nodes[0].AsClassDeclaration()
assert.Assert(t, classDecl.Name() != nil)
assert.Equal(t, classDecl.Name().AsIdentifier().Text, "Foo")
assert.Equal(t, classDecl.Name().AsIdentifier().Text.Value(), "Foo")
assert.Assert(t, classDecl.Members != nil)
assert.Equal(t, len(classDecl.Members.Nodes), 1)
assert.Equal(t, classDecl.Members.Nodes[0].Kind, ast.KindMethodDeclaration)
Expand Down Expand Up @@ -323,7 +323,7 @@ func TestDecodeNodes_SubtreeRoundTrip(t *testing.T) {
assert.Equal(t, decoded.Kind, ast.KindFunctionDeclaration)
funcDecl := decoded.AsFunctionDeclaration()
assert.Assert(t, funcDecl.Name() != nil)
assert.Equal(t, funcDecl.Name().AsIdentifier().Text, "greet")
assert.Equal(t, funcDecl.Name().AsIdentifier().Text.Value(), "greet")
assert.Assert(t, funcDecl.Parameters != nil)
assert.Equal(t, len(funcDecl.Parameters.Nodes), 1)
assert.Assert(t, funcDecl.Body != nil)
Expand Down
2 changes: 1 addition & 1 deletion tsc/internal/api/encoder/encoder_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions tsc/internal/api/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -2903,7 +2903,7 @@ func (s *Session) handleResolveName(ctx context.Context, params *ResolveNamePara
return nil, err
}

symbol := setup.checker.ResolveName(params.Name, location, ast.SymbolFlags(params.Meaning), params.ExcludeGlobals)
symbol := setup.checker.ResolveName(ast.MakeSymbolName(params.Name), location, ast.SymbolFlags(params.Meaning), params.ExcludeGlobals)
if symbol == nil {
return nil, nil
}
Expand Down Expand Up @@ -3482,7 +3482,7 @@ func (s *Session) resolveSymbolTablePropertyOfSymbol(ctx context.Context, params
return order
}
}
if order := cmp.Compare(left.Name(), right.Name()); order != 0 {
if order := cmp.Compare(left.Name().Value(), right.Name().Value()); order != 0 {
return order
}
return cmp.Compare(ast.GetSymbolId(left), ast.GetSymbolId(right))
Expand Down Expand Up @@ -4314,7 +4314,7 @@ func (s *Session) handleGetTypePredicateOfSignature(ctx context.Context, params
resp := &TypePredicateResponse{
Kind: int32(pred.Kind()),
ParameterIndex: pred.ParameterIndex(),
ParameterName: pred.ParameterName(),
ParameterName: pred.ParameterName().Value(),
}
if pred.Type() != nil {
resp.Type = setup.sd.newTypeResponse(setup.projectID, pred.Type(), setup.checker)
Expand Down Expand Up @@ -4600,7 +4600,7 @@ func (s *Session) handleGetPropertyOfType(ctx context.Context, params *GetProper
return nil, err
}

prop := setup.checker.GetPropertyOfType(t, params.Name)
prop := setup.checker.GetPropertyOfType(t, ast.MakeSymbolName(params.Name))
if prop == nil {
return nil, nil
}
Expand All @@ -4621,7 +4621,7 @@ func (s *Session) handleGetTypeOfPropertyOfType(ctx context.Context, params *Get
return nil, err
}

return setup.sd.newTypeResponse(setup.projectID, setup.checker.GetTypeOfPropertyOfType(t, params.Name), setup.checker), nil
return setup.sd.newTypeResponse(setup.projectID, setup.checker.GetTypeOfPropertyOfType(t, ast.MakeSymbolName(params.Name)), setup.checker), nil
}

// handleGetConstantValue returns the constant value of an enum member or const enum access.
Expand Down Expand Up @@ -4892,7 +4892,7 @@ func (s *Session) handleGetMemberInModuleExports(ctx context.Context, params *Ge
return nil, nil
}

member := setup.checker.TryGetMemberInModuleExports(params.Name, symbol)
member := setup.checker.TryGetMemberInModuleExports(ast.MakeSymbolName(params.Name), symbol)
if member == nil {
return nil, nil
}
Expand Down
6 changes: 3 additions & 3 deletions tsc/internal/api/session_symbolresponse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func TestTransientSymbolWithFileDeclarationIsSnapshotOwned(t *testing.T) {
class := sourceFile.Statements.Nodes[0]
symbol := ast.NewSymbol()
symbol.SetFlags(ast.SymbolFlagsClass | ast.SymbolFlagsTransient)
symbol.SetName("C")
symbol.SetName(ast.MakeSymbolName("C"))
symbol.SetDeclarations([]*ast.Node{class})
sd := newTestSnapshotData()

Expand All @@ -85,7 +85,7 @@ func TestSymbolReferencesIdentifyOwnerWithoutDescriptor(t *testing.T) {
assert.Assert(t, !strings.Contains(string(encoded), "contentHash"))

// A file-owned symbol's relationships are references into the same file.
member := class.Members()["property"]
member := class.Members()[ast.MakeSymbolName("property")]
response := newFileSymbolResponse(member)
assert.DeepEqual(t, response.Parent, reference)
}
Expand All @@ -109,6 +109,6 @@ func TestContentMappedSymbolsAreSnapshotOwned(t *testing.T) {
assert.NilError(t, err)
assert.Equal(t, resolved, class)

reference := newSymbolReference(class.Members()["property"])
reference := newSymbolReference(class.Members()[ast.MakeSymbolName("property")])
assert.Equal(t, reference.File, "")
}
11 changes: 10 additions & 1 deletion tsc/internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,19 @@ func (n *Node) Body() *Node {
return nil
}

// TextSymbolName returns Text() as a SymbolName. Identifiers are interned when
// created, so this avoids reinterning their text.
func (n *Node) TextSymbolName() SymbolName {
if n.Kind == KindIdentifier {
return n.AsIdentifier().Text
}
return MakeSymbolName(n.Text())
}

func (n *Node) Text() string {
switch n.Kind {
case KindIdentifier:
return n.AsIdentifier().Text
return n.AsIdentifier().Text.Value()
case KindPrivateIdentifier:
return n.AsPrivateIdentifier().Text
case KindStringLiteral:
Expand Down
Loading
Loading