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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/typescript/src/api/node/protocol.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export const childProperties: Readonly<Partial<Record<SyntaxKind, readonly (stri
[SyntaxKind.JSDocSignature]: ["typeParameters", "parameters", "type"],
[SyntaxKind.JSDocNameReference]: ["name"],
[SyntaxKind.SourceFile]: ["statements", "endOfFileToken"],
[SyntaxKind.ModuleDeclaration]: ["modifiers", "name", "body"],
[SyntaxKind.ModuleDeclaration]: ["modifiers", "name", "attributes", "body"],
[SyntaxKind.ImportEqualsDeclaration]: ["modifiers", "name", "moduleReference"],
[SyntaxKind.ExportDeclaration]: ["modifiers", "exportClause", "moduleSpecifier", "attributes"],
[SyntaxKind.ImportType]: ["argument", "attributes", "qualifier", "typeArguments"],
Expand Down
1 change: 1 addition & 0 deletions packages/typescript/src/ast/ast.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,7 @@ export interface ModuleDeclaration extends StatementBase, DeclarationBase, Modif
readonly kind: SyntaxKind.ModuleDeclaration;
readonly keyword: SyntaxKind.ModuleKeyword | SyntaxKind.NamespaceKeyword;
readonly name: ModuleName;
readonly attributes?: TypeLiteralNode;
readonly body?: ModuleBody;
}
export interface ImportEqualsDeclaration extends StatementBase, DeclarationBase, ModifiersBase {
Expand Down
10 changes: 6 additions & 4 deletions packages/typescript/src/ast/factory.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1075,7 +1075,7 @@ function cloneNodeData(node: Node): any {
case SyntaxKind.JSDocNameReference:
return { name: n.name };
case SyntaxKind.ModuleDeclaration:
return { modifiers: n.modifiers, keyword: n.keyword, name: n.name, body: n.body };
return { modifiers: n.modifiers, keyword: n.keyword, name: n.name, attributes: n.attributes, body: n.body };
case SyntaxKind.ImportEqualsDeclaration:
return { modifiers: n.modifiers, isTypeOnly: n.isTypeOnly, name: n.name, moduleReference: n.moduleReference };
case SyntaxKind.ExportDeclaration:
Expand Down Expand Up @@ -1605,6 +1605,7 @@ const forEachChildTable: Record<number, ForEachChildFunction> = {
[SyntaxKind.ModuleDeclaration]: (data, cbNode, cbNodes) =>
visitNodes(cbNode, cbNodes, data.modifiers) ||
visitNode(cbNode, data.name) ||
visitNode(cbNode, data.attributes) ||
visitNode(cbNode, data.body),
[SyntaxKind.ImportEqualsDeclaration]: (data, cbNode, cbNodes) =>
visitNodes(cbNode, cbNodes, data.modifiers) ||
Expand Down Expand Up @@ -2955,11 +2956,12 @@ export function createJSDocNameReference(name: EntityName): JSDocNameReference {
}) as unknown as JSDocNameReference;
}

export function createModuleDeclaration(modifiers: readonly ModifierLike[] | undefined, keyword: SyntaxKind.ModuleKeyword | SyntaxKind.NamespaceKeyword, name: ModuleName, body?: ModuleBody): ModuleDeclaration {
export function createModuleDeclaration(modifiers: readonly ModifierLike[] | undefined, keyword: SyntaxKind.ModuleKeyword | SyntaxKind.NamespaceKeyword, name: ModuleName, attributes?: TypeLiteralNode, body?: ModuleBody): ModuleDeclaration {
return new NodeObject(SyntaxKind.ModuleDeclaration, {
modifiers: modifiers ? createNodeArray(modifiers) : undefined,
keyword,
name,
attributes,
body,
}) as unknown as ModuleDeclaration;
}
Expand Down Expand Up @@ -3726,8 +3728,8 @@ export function updateJSDocNameReference(node: JSDocNameReference, name: EntityN
return node.name !== name ? createJSDocNameReference(name) : node;
}

export function updateModuleDeclaration(node: ModuleDeclaration, modifiers: readonly ModifierLike[] | undefined, name: ModuleName, body?: ModuleBody): ModuleDeclaration {
return node.modifiers !== modifiers || node.name !== name || node.body !== body ? createModuleDeclaration(modifiers, node.keyword, name, body) : node;
export function updateModuleDeclaration(node: ModuleDeclaration, modifiers: readonly ModifierLike[] | undefined, name: ModuleName, attributes?: TypeLiteralNode, body?: ModuleBody): ModuleDeclaration {
return node.modifiers !== modifiers || node.name !== name || node.attributes !== attributes || node.body !== body ? createModuleDeclaration(modifiers, node.keyword, name, attributes, body) : node;
}

export function updateImportEqualsDeclaration(node: ImportEqualsDeclaration, modifiers: readonly ModifierLike[] | undefined, name: Identifier, moduleReference: ModuleReference): ImportEqualsDeclaration {
Expand Down
16 changes: 16 additions & 0 deletions packages/typescript/src/ast/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ export function escapeLeadingUnderscores(identifier: string): __String {
: identifier) as __String;
}

/**
* Gets the module specifier represented by an ambient module symbol's escaped
* name, or `undefined` when the name does not identify an ambient module.
*/
export function tryGetAmbientModuleNameFromSymbolName(name: __String): string | undefined {
const text = name as string;
if (text.charCodeAt(0) === CharacterCodes.doubleQuote && text.charCodeAt(text.length - 1) === CharacterCodes.doubleQuote) {
return text.slice(1, -1);
}

const patternPrefix = '__"';
if (!text.startsWith(patternPrefix)) return undefined;
const markerIndex = text.lastIndexOf('"pattern@');
return markerIndex > patternPrefix.length ? text.slice(patternPrefix.length, markerIndex) : undefined;
}

export function tryCast<TOut extends TIn, TIn = any>(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut | undefined {
return value !== undefined && test(value) ? value : undefined;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/typescript/src/ast/visitor.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ import {
isTemplateHead,
isTemplateLiteral,
isTemplateMiddleOrTail,
isTypeLiteralNode,
isTypeNode,
isTypeParameterDeclaration,
isTypePredicateParameterName,
Expand Down Expand Up @@ -1342,8 +1343,9 @@ const visitEachChildTable: Record<number, VisitEachChildFunction> = {
[SyntaxKind.ModuleDeclaration]: (node: ModuleDeclaration, visitor: Visitor): ModuleDeclaration => {
const _modifiers = visitNodes(node.modifiers, visitor);
const _name = visitNode(node.name, visitor, isModuleName);
const _attributes = visitNode(node.attributes, visitor, isTypeLiteralNode);
const _body = visitNode(node.body, visitor, isModuleBody);
return updateModuleDeclaration(node, _modifiers, _name, _body);
return updateModuleDeclaration(node, _modifiers, _name, _attributes, _body);
},
[SyntaxKind.ImportEqualsDeclaration]: (node: ImportEqualsDeclaration, visitor: Visitor): ImportEqualsDeclaration => {
const _modifiers = visitNodes(node.modifiers, visitor);
Expand Down
58 changes: 58 additions & 0 deletions packages/typescript/test/async/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
isImportDeclaration,
isInterfaceDeclaration,
isJSDocParameterTag,
isModuleDeclaration,
isNamedImports,
isReturnStatement,
isShorthandPropertyAssignment,
Expand All @@ -28,6 +29,7 @@ import {
type NodeArray,
NodeFlags,
SyntaxKind,
tryGetAmbientModuleNameFromSymbolName,
unescapeLeadingUnderscores,
} from "@typescript/typescript/unstable/ast";
import {
Expand Down Expand Up @@ -4294,6 +4296,52 @@ export const obj: { a: number } = { a: 1 };
await api.close();
}
});

test("distinguishes a pattern ambient module name from a matching user-provided export name", async () => {
const maliciousName = '"*.css"__pattern@1234';
const types = `
declare module "*.css" with { type: "css" } {
const className: string;
export default className;
}
`;
const source = `
const x = "";
export { x as '${maliciousName}' };
`;
const api = spawnAPI({
"/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true, module: "preserve" } }),
"/src/types.d.ts": types,
"/src/main.ts": source,
});
try {
const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" });
const project = snapshot.getProject("/tsconfig.json")!;

const typesFile = await project.program.getSourceFile("/src/types.d.ts");
assert.ok(typesFile);
const moduleDeclaration = typesFile.statements.find(isModuleDeclaration);
assert.ok(moduleDeclaration);
const ambientModule = await project.checker.getSymbolAtLocation(moduleDeclaration.name);
assert.ok(ambientModule);
assert.match(ambientModule.name, /^__"\*\.css"pattern@\d+$/);
assert.equal(ambientModule.escapedName, ambientModule.name);
assert.equal(tryGetAmbientModuleNameFromSymbolName(ambientModule.escapedName), "*.css");

const sourceFile = await project.program.getSourceFile("/src/main.ts");
assert.ok(sourceFile);
const sourceFileSymbol = await project.checker.getSymbolAtLocation(sourceFile);
assert.ok(sourceFileSymbol);
const maliciousExport = (await sourceFileSymbol.getExports()).get(escapeLeadingUnderscores(maliciousName));
assert.ok(maliciousExport);
assert.equal(maliciousExport.name, maliciousName);
assert.equal(maliciousExport.escapedName, maliciousName);
assert.equal(tryGetAmbientModuleNameFromSymbolName(maliciousExport.escapedName), undefined);
}
finally {
await api.close();
}
});
});

describe("ast - escapeLeadingUnderscores", () => {
Expand All @@ -4307,6 +4355,16 @@ describe("ast - escapeLeadingUnderscores", () => {
});
});

describe("ast - tryGetAmbientModuleNameFromSymbolName", () => {
test("gets ambient module names from escaped symbol names", () => {
assert.equal(tryGetAmbientModuleNameFromSymbolName('"pkg"' as __String), "pkg");
assert.equal(tryGetAmbientModuleNameFromSymbolName('__"*.css"pattern@1234' as __String), "*.css");
assert.equal(tryGetAmbientModuleNameFromSymbolName('"*.css"__pattern@1234' as __String), undefined);
assert.equal(tryGetAmbientModuleNameFromSymbolName('___"*.css"pattern@1234' as __String), undefined);
assert.equal(tryGetAmbientModuleNameFromSymbolName("value" as __String), undefined);
});
});

describe("ast - getJSDocTags", () => {
test("returns a node's own tags, and inherited @param / @template tags", async () => {
const api = spawnAPI({
Expand Down
58 changes: 58 additions & 0 deletions packages/typescript/test/sync/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
isImportDeclaration,
isInterfaceDeclaration,
isJSDocParameterTag,
isModuleDeclaration,
isNamedImports,
isReturnStatement,
isShorthandPropertyAssignment,
Expand All @@ -36,6 +37,7 @@ import {
type NodeArray,
NodeFlags,
SyntaxKind,
tryGetAmbientModuleNameFromSymbolName,
unescapeLeadingUnderscores,
} from "@typescript/typescript/unstable/ast";
import {
Expand Down Expand Up @@ -4210,6 +4212,52 @@ export const obj: { a: number } = { a: 1 };
api.close();
}
});

test("distinguishes a pattern ambient module name from a matching user-provided export name", () => {
const maliciousName = '"*.css"__pattern@1234';
const types = `
declare module "*.css" with { type: "css" } {
const className: string;
export default className;
}
`;
const source = `
const x = "";
export { x as '${maliciousName}' };
`;
const api = spawnAPI({
"/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true, module: "preserve" } }),
"/src/types.d.ts": types,
"/src/main.ts": source,
});
try {
const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" });
const project = snapshot.getProject("/tsconfig.json")!;

const typesFile = project.program.getSourceFile("/src/types.d.ts");
assert.ok(typesFile);
const moduleDeclaration = typesFile.statements.find(isModuleDeclaration);
assert.ok(moduleDeclaration);
const ambientModule = project.checker.getSymbolAtLocation(moduleDeclaration.name);
assert.ok(ambientModule);
assert.match(ambientModule.name, /^__"\*\.css"pattern@\d+$/);
assert.equal(ambientModule.escapedName, ambientModule.name);
assert.equal(tryGetAmbientModuleNameFromSymbolName(ambientModule.escapedName), "*.css");

const sourceFile = project.program.getSourceFile("/src/main.ts");
assert.ok(sourceFile);
const sourceFileSymbol = project.checker.getSymbolAtLocation(sourceFile);
assert.ok(sourceFileSymbol);
const maliciousExport = (sourceFileSymbol.getExports()).get(escapeLeadingUnderscores(maliciousName));
assert.ok(maliciousExport);
assert.equal(maliciousExport.name, maliciousName);
assert.equal(maliciousExport.escapedName, maliciousName);
assert.equal(tryGetAmbientModuleNameFromSymbolName(maliciousExport.escapedName), undefined);
}
finally {
api.close();
}
});
});

describe("ast - escapeLeadingUnderscores", () => {
Expand All @@ -4223,6 +4271,16 @@ describe("ast - escapeLeadingUnderscores", () => {
});
});

describe("ast - tryGetAmbientModuleNameFromSymbolName", () => {
test("gets ambient module names from escaped symbol names", () => {
assert.equal(tryGetAmbientModuleNameFromSymbolName('"pkg"' as __String), "pkg");
assert.equal(tryGetAmbientModuleNameFromSymbolName('__"*.css"pattern@1234' as __String), "*.css");
assert.equal(tryGetAmbientModuleNameFromSymbolName('"*.css"__pattern@1234' as __String), undefined);
assert.equal(tryGetAmbientModuleNameFromSymbolName('___"*.css"pattern@1234' as __String), undefined);
assert.equal(tryGetAmbientModuleNameFromSymbolName("value" as __String), undefined);
});
});

describe("ast - getJSDocTags", () => {
test("returns a node's own tags, and inherited @param / @template tags", () => {
const api = spawnAPI({
Expand Down
5 changes: 5 additions & 0 deletions tools/scripts/tsc/ast.json
Original file line number Diff line number Diff line change
Expand Up @@ -4807,6 +4807,11 @@
"type": "ModuleName",
"private": true
},
{
"name": "Attributes",
"type": "TypeLiteralNode",
"optional": true
},
{
"name": "Body",
"inherited": true,
Expand Down
5 changes: 3 additions & 2 deletions tsc/internal/api/encoder/decoder_generated.go

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

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.

37 changes: 12 additions & 25 deletions tsc/internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/microsoft/TypeScript/tsc/internal/collections"
"github.com/microsoft/TypeScript/tsc/internal/core"
"github.com/microsoft/TypeScript/tsc/internal/diagnostics"
"github.com/microsoft/TypeScript/tsc/internal/spanmap"
"github.com/microsoft/TypeScript/tsc/internal/tspath"
"github.com/zeebo/xxh3"
Expand Down Expand Up @@ -968,6 +969,8 @@ func (n *Node) Attributes() *Node {
return n.AsJsxOpeningElement().Attributes
case KindJsxSelfClosingElement:
return n.AsJsxSelfClosingElement().Attributes
case KindModuleDeclaration:
return n.AsModuleDeclaration().Attributes
}
panic("Unhandled case in Node.Attributes: " + n.Kind.String())
}
Expand Down Expand Up @@ -2211,44 +2214,28 @@ func (node *ExpressionWithTypeArguments) computeSubtreeFacts() SubtreeFacts {
propagateEraseableSyntaxListSubtreeFacts(node.TypeArguments)
}

func (node *ImportAttributesNode) GetResolutionModeOverride( /* !!! grammarErrorOnNode?: (node: Node, diagnostic: DiagnosticMessage) => void*/ ) (core.ResolutionMode, bool) {
func (node *ImportAttributesNode) GetResolutionModeOverride(grammarErrorOnNode func(node *Node, message *diagnostics.Message, args ...any) bool) (core.ResolutionMode, bool) {
if node == nil {
return core.ResolutionModeNone, false
}

attributes := node.AsImportAttributes().Attributes

if len(attributes.Nodes) != 1 {
// !!!
// grammarErrorOnNode?.(
// node,
// node.token === SyntaxKind.WithKeyword
// ? Diagnostics.Type_import_attributes_should_have_exactly_one_key_resolution_mode_with_value_import_or_require
// : Diagnostics.Type_import_assertions_should_have_exactly_one_key_resolution_mode_with_value_import_or_require,
// );
attribute := core.Find(attributes.Nodes, func(attribute *Node) bool {
return attribute.Name().Text() == "resolution-mode"
})
if attribute == nil {
return core.ResolutionModeNone, false
}

elem := attributes.Nodes[0].AsImportAttribute()
if !IsStringLiteralLike(elem.Name()) {
return core.ResolutionModeNone, false
}
if elem.Name().Text() != "resolution-mode" {
// !!!
// grammarErrorOnNode?.(
// elem.name,
// node.token === SyntaxKind.WithKeyword
// ? Diagnostics.resolution_mode_is_the_only_valid_key_for_type_import_attributes
// : Diagnostics.resolution_mode_is_the_only_valid_key_for_type_import_assertions,
// );
return core.ResolutionModeNone, false
}
elem := attribute.AsImportAttribute()
if !IsStringLiteralLike(elem.Value) {
return core.ResolutionModeNone, false
}
if elem.Value.Text() != "import" && elem.Value.Text() != "require" {
// !!!
// grammarErrorOnNode?.(elem.value, Diagnostics.resolution_mode_should_be_either_require_or_import);
if grammarErrorOnNode != nil {
grammarErrorOnNode(elem.Value, diagnostics.X_resolution_mode_should_be_either_require_or_import)
}
return core.ResolutionModeNone, false
}
if elem.Value.Text() == "import" {
Expand Down
Loading