Skip to content

Commit 6d458e8

Browse files
Deduplicate protocol.ts content (microsoft#57361)
Co-authored-by: Jake Bailey <[email protected]>
1 parent 64edd07 commit 6d458e8

18 files changed

+1029
-1896
lines changed

scripts/dtsBundler.mjs

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -214,28 +214,35 @@ function nodeToLocation(node) {
214214

215215
/**
216216
* @param {ts.Node} node
217+
* @param {boolean} needExportModifier
217218
* @returns {ts.Node | undefined}
218219
*/
219-
function removeDeclareConstExport(node) {
220+
function removeDeclareConstExport(node, needExportModifier) {
220221
switch (node.kind) {
221222
case ts.SyntaxKind.DeclareKeyword: // No need to emit this in d.ts files.
222223
case ts.SyntaxKind.ConstKeyword: // Remove const from const enums.
223-
case ts.SyntaxKind.ExportKeyword: // No export modifier; we are already in the namespace.
224224
return undefined;
225+
case ts.SyntaxKind.ExportKeyword: // No export modifier; we are already in the namespace.
226+
if (!needExportModifier) {
227+
return undefined;
228+
}
225229
}
226230
return node;
227231
}
228232

229-
/** @type {Map<string, ts.Symbol>[]} */
233+
/** @type {{ locals: Map<string, { symbol: ts.Symbol, writeTarget: WriteTarget }>, exports: Map<string, ts.Symbol>}[]} */
230234
const scopeStack = [];
231235

236+
/** @type {Map<ts.Symbol, string>} */
237+
const symbolToNamespace = new Map();
238+
232239
/**
233240
* @param {string} name
234241
*/
235242
function findInScope(name) {
236243
for (let i = scopeStack.length - 1; i >= 0; i--) {
237244
const scope = scopeStack[i];
238-
const symbol = scope.get(name);
245+
const symbol = scope.exports.get(name);
239246
if (symbol) {
240247
return symbol;
241248
}
@@ -290,8 +297,9 @@ function symbolsConflict(s1, s2) {
290297

291298
/**
292299
* @param {ts.Statement} decl
300+
* @param {boolean} isInternal
293301
*/
294-
function verifyMatchingSymbols(decl) {
302+
function verifyMatchingSymbols(decl, isInternal) {
295303
ts.visitEachChild(decl, /** @type {(node: ts.Node) => ts.Node} */ function visit(node) {
296304
if (ts.isIdentifier(node) && ts.isPartOfTypeNode(node)) {
297305
if (ts.isQualifiedName(node.parent) && node !== node.parent.left) {
@@ -310,6 +318,10 @@ function verifyMatchingSymbols(decl) {
310318
}
311319
const symbolInScope = findInScope(symbolOfNode.name);
312320
if (!symbolInScope) {
321+
if (symbolOfNode.declarations?.every(d => isLocalDeclaration(d) && d.getSourceFile() === decl.getSourceFile()) && !isSelfReference(node, symbolOfNode)) {
322+
// The symbol is a local that needs to be copied into the scope.
323+
scopeStack[scopeStack.length - 1].locals.set(symbolOfNode.name, { symbol: symbolOfNode, writeTarget: isInternal ? WriteTarget.Internal : WriteTarget.Both });
324+
}
313325
// We didn't find the symbol in scope at all. Just allow it and we'll fail at test time.
314326
return node;
315327
}
@@ -323,39 +335,72 @@ function verifyMatchingSymbols(decl) {
323335
}, /*context*/ undefined);
324336
}
325337

338+
/**
339+
* @param {ts.Declaration} decl
340+
*/
341+
function isLocalDeclaration(decl) {
342+
return ts.canHaveModifiers(decl)
343+
&& !ts.getModifiers(decl)?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)
344+
&& !!getDeclarationStatement(decl);
345+
}
346+
347+
/**
348+
* @param {ts.Node} reference
349+
* @param {ts.Symbol} symbol
350+
*/
351+
function isSelfReference(reference, symbol) {
352+
return symbol.declarations?.every(parent => ts.findAncestor(reference, p => p === parent));
353+
}
354+
326355
/**
327356
* @param {string} name
357+
* @param {string} parent
358+
* @param {boolean} needExportModifier
328359
* @param {ts.Symbol} moduleSymbol
329360
*/
330-
function emitAsNamespace(name, moduleSymbol) {
361+
function emitAsNamespace(name, parent, moduleSymbol, needExportModifier) {
331362
assert(moduleSymbol.flags & ts.SymbolFlags.ValueModule, "moduleSymbol is not a module");
332363

333-
scopeStack.push(new Map());
364+
const fullName = parent ? `${parent}.${name}` : name;
365+
366+
scopeStack.push({ locals: new Map(), exports: new Map() });
334367
const currentScope = scopeStack[scopeStack.length - 1];
335368

336369
const target = containsPublicAPI(moduleSymbol) ? WriteTarget.Both : WriteTarget.Internal;
337370

338371
if (name === "ts") {
339372
// We will write `export = ts` at the end.
373+
assert(!needExportModifier, "ts namespace should not have an export modifier");
340374
write(`declare namespace ${name} {`, target);
341375
}
342376
else {
343-
// No export modifier; we are already in the namespace.
344-
write(`namespace ${name} {`, target);
377+
write(`${needExportModifier ? "export " : ""}namespace ${name} {`, target);
345378
}
346379
increaseIndent();
347380

348381
const moduleExports = typeChecker.getExportsOfModule(moduleSymbol);
349382
for (const me of moduleExports) {
350-
currentScope.set(me.name, me);
383+
currentScope.exports.set(me.name, me);
384+
symbolToNamespace.set(me, fullName);
351385
}
352386

387+
/** @type {[ts.Statement, ts.SourceFile, WriteTarget][]} */
388+
const exportedStatements = [];
389+
/** @type {[name: string, fullName: string, moduleSymbol: ts.Symbol][]} */
390+
const nestedNamespaces = [];
353391
for (const me of moduleExports) {
354392
assert(me.declarations?.length);
355393

356394
if (me.flags & ts.SymbolFlags.Alias) {
357395
const resolved = typeChecker.getAliasedSymbol(me);
358-
emitAsNamespace(me.name, resolved);
396+
if (resolved.flags & ts.SymbolFlags.ValueModule) {
397+
nestedNamespaces.push([me.name, fullName, resolved]);
398+
}
399+
else {
400+
const namespaceName = symbolToNamespace.get(resolved);
401+
assert(namespaceName, `Failed to find namespace for ${me.name} at ${nodeToLocation(me.declarations[0])}`);
402+
write(`export import ${me.name} = ${namespaceName}.${me.name}`, target);
403+
}
359404
continue;
360405
}
361406

@@ -367,34 +412,60 @@ function emitAsNamespace(name, moduleSymbol) {
367412
fail(`Unhandled declaration for ${me.name} at ${nodeToLocation(decl)}`);
368413
}
369414

370-
verifyMatchingSymbols(statement);
371-
372415
const isInternal = ts.isInternalDeclaration(statement);
416+
if (!ts.isModuleDeclaration(decl)) {
417+
verifyMatchingSymbols(statement, isInternal);
418+
}
419+
373420
if (!isInternal) {
374421
const publicStatement = ts.visitEachChild(statement, node => {
375422
// No @internal comments in the public API.
376423
if (ts.isInternalDeclaration(node)) {
377424
return undefined;
378425
}
379-
return removeDeclareConstExport(node);
426+
return node;
380427
}, /*context*/ undefined);
381428

382-
writeNode(publicStatement, sourceFile, WriteTarget.Public);
429+
exportedStatements.push([publicStatement, sourceFile, WriteTarget.Public]);
383430
}
384431

385-
const internalStatement = ts.visitEachChild(statement, removeDeclareConstExport, /*context*/ undefined);
386-
387-
writeNode(internalStatement, sourceFile, WriteTarget.Internal);
432+
exportedStatements.push([statement, sourceFile, WriteTarget.Internal]);
388433
}
389434
}
390435

436+
const childrenNeedExportModifier = !!currentScope.locals.size;
437+
438+
nestedNamespaces.forEach(namespace => emitAsNamespace(...namespace, childrenNeedExportModifier));
439+
440+
currentScope.locals.forEach(({ symbol, writeTarget }) => {
441+
symbol.declarations?.forEach(decl => {
442+
// We already checked that getDeclarationStatement(decl) works for each declaration.
443+
const statement = getDeclarationStatement(decl);
444+
writeNode(/** @type {ts.Statement} */ (statement), decl.getSourceFile(), writeTarget);
445+
});
446+
});
447+
448+
exportedStatements.forEach(([statement, ...rest]) => {
449+
let updated = ts.visitEachChild(statement, node => removeDeclareConstExport(node, childrenNeedExportModifier), /*context*/ undefined);
450+
if (childrenNeedExportModifier && ts.canHaveModifiers(updated) && !updated.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) {
451+
updated = ts.factory.replaceModifiers(
452+
updated,
453+
[
454+
ts.factory.createModifier(ts.SyntaxKind.ExportKeyword),
455+
.../**@type {ts.NodeArray<ts.Modifier> | undefined}*/ (updated.modifiers) ?? [],
456+
],
457+
);
458+
}
459+
writeNode(updated, ...rest);
460+
});
461+
391462
scopeStack.pop();
392463

393464
decreaseIndent();
394465
write(`}`, target);
395466
}
396467

397-
emitAsNamespace("ts", moduleSymbol);
468+
emitAsNamespace("ts", "", moduleSymbol, /*needExportModifier*/ false);
398469

399470
write("export = ts;", WriteTarget.Both);
400471

src/compiler/types.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9845,13 +9845,49 @@ export interface CommentDirectivesMap {
98459845
export interface UserPreferences {
98469846
readonly disableSuggestions?: boolean;
98479847
readonly quotePreference?: "auto" | "double" | "single";
9848+
/**
9849+
* If enabled, TypeScript will search through all external modules' exports and add them to the completions list.
9850+
* This affects lone identifier completions but not completions on the right hand side of `obj.`.
9851+
*/
98489852
readonly includeCompletionsForModuleExports?: boolean;
9853+
/**
9854+
* Enables auto-import-style completions on partially-typed import statements. E.g., allows
9855+
* `import write|` to be completed to `import { writeFile } from "fs"`.
9856+
*/
98499857
readonly includeCompletionsForImportStatements?: boolean;
9858+
/**
9859+
* Allows completions to be formatted with snippet text, indicated by `CompletionItem["isSnippet"]`.
9860+
*/
98509861
readonly includeCompletionsWithSnippetText?: boolean;
9862+
/**
9863+
* Unless this option is `false`, or `includeCompletionsWithInsertText` is not enabled,
9864+
* member completion lists triggered with `.` will include entries on potentially-null and potentially-undefined
9865+
* values, with insertion text to replace preceding `.` tokens with `?.`.
9866+
*/
98519867
readonly includeAutomaticOptionalChainCompletions?: boolean;
9868+
/**
9869+
* If enabled, the completion list will include completions with invalid identifier names.
9870+
* For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`.
9871+
*/
98529872
readonly includeCompletionsWithInsertText?: boolean;
9873+
/**
9874+
* If enabled, completions for class members (e.g. methods and properties) will include
9875+
* a whole declaration for the member.
9876+
* E.g., `class A { f| }` could be completed to `class A { foo(): number {} }`, instead of
9877+
* `class A { foo }`.
9878+
*/
98539879
readonly includeCompletionsWithClassMemberSnippets?: boolean;
9880+
/**
9881+
* If enabled, object literal methods will have a method declaration completion entry in addition
9882+
* to the regular completion entry containing just the method name.
9883+
* E.g., `const objectLiteral: T = { f| }` could be completed to `const objectLiteral: T = { foo(): void {} }`,
9884+
* in addition to `const objectLiteral: T = { foo }`.
9885+
*/
98549886
readonly includeCompletionsWithObjectLiteralMethodSnippets?: boolean;
9887+
/**
9888+
* Indicates whether {@link CompletionEntry.labelDetails completion entry label details} are supported.
9889+
* If not, contents of `labelDetails` may be included in the {@link CompletionEntry.name} property.
9890+
*/
98559891
readonly useLabelDetailsInCompletionEntries?: boolean;
98569892
readonly allowIncompleteCompletions?: boolean;
98579893
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
@@ -9874,14 +9910,74 @@ export interface UserPreferences {
98749910
readonly allowRenameOfImportPath?: boolean;
98759911
readonly autoImportFileExcludePatterns?: string[];
98769912
readonly preferTypeOnlyAutoImports?: boolean;
9913+
/**
9914+
* Indicates whether imports should be organized in a case-insensitive manner.
9915+
*/
98779916
readonly organizeImportsIgnoreCase?: "auto" | boolean;
9917+
/**
9918+
* Indicates whether imports should be organized via an "ordinal" (binary) comparison using the numeric value
9919+
* of their code points, or via "unicode" collation (via the
9920+
* [Unicode Collation Algorithm](https://unicode.org/reports/tr10/#Scope)) using rules associated with the locale
9921+
* specified in {@link organizeImportsCollationLocale}.
9922+
*
9923+
* Default: `"ordinal"`.
9924+
*/
98789925
readonly organizeImportsCollation?: "ordinal" | "unicode";
9926+
/**
9927+
* Indicates the locale to use for "unicode" collation. If not specified, the locale `"en"` is used as an invariant
9928+
* for the sake of consistent sorting. Use `"auto"` to use the detected UI locale.
9929+
*
9930+
* This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`.
9931+
*
9932+
* Default: `"en"`
9933+
*/
98799934
readonly organizeImportsLocale?: string;
9935+
/**
9936+
* Indicates whether numeric collation should be used for digit sequences in strings. When `true`, will collate
9937+
* strings such that `a1z < a2z < a100z`. When `false`, will collate strings such that `a1z < a100z < a2z`.
9938+
*
9939+
* This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`.
9940+
*
9941+
* Default: `false`
9942+
*/
98809943
readonly organizeImportsNumericCollation?: boolean;
9944+
/**
9945+
* Indicates whether accents and other diacritic marks are considered unequal for the purpose of collation. When
9946+
* `true`, characters with accents and other diacritics will be collated in the order defined by the locale specified
9947+
* in {@link organizeImportsCollationLocale}.
9948+
*
9949+
* This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`.
9950+
*
9951+
* Default: `true`
9952+
*/
98819953
readonly organizeImportsAccentCollation?: boolean;
9954+
/**
9955+
* Indicates whether upper case or lower case should sort first. When `false`, the default order for the locale
9956+
* specified in {@link organizeImportsCollationLocale} is used.
9957+
*
9958+
* This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`. This preference is also
9959+
* ignored if we are using case-insensitive sorting, which occurs when {@link organizeImportsIgnoreCase} is `true`,
9960+
* or if {@link organizeImportsIgnoreCase} is `"auto"` and the auto-detected case sensitivity is determined to be
9961+
* case-insensitive.
9962+
*
9963+
* Default: `false`
9964+
*/
98829965
readonly organizeImportsCaseFirst?: "upper" | "lower" | false;
9966+
/**
9967+
* Indicates where named type-only imports should sort. "inline" sorts named imports without regard to if the import is
9968+
* type-only.
9969+
*
9970+
* Default: `last`
9971+
*/
98839972
readonly organizeImportsTypeOrder?: "first" | "last" | "inline";
9973+
/**
9974+
* Indicates whether to exclude standard library and node_modules file symbols from navTo results.
9975+
*/
98849976
readonly excludeLibrarySymbolsInNavTo?: boolean;
9977+
readonly lazyConfiguredProjectsFromExternalProject?: boolean;
9978+
readonly displayPartsForJSDoc?: boolean;
9979+
readonly generateReturnInDocTemplate?: boolean;
9980+
readonly disableLineTextInReferences?: boolean;
98859981
}
98869982

98879983
/** Represents a bigint literal value without requiring bigint support */

0 commit comments

Comments
 (0)