Skip to content

Commit c57ff08

Browse files
author
Andy
authored
Add codefix to generate types for untyped module (#26588)
1 parent 7852cf7 commit c57ff08

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2117
-116
lines changed

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"gulp-typescript": "latest",
7676
"istanbul": "latest",
7777
"jake": "latest",
78+
"lodash": "4.17.10",
7879
"merge2": "latest",
7980
"minimist": "latest",
8081
"mkdirp": "latest",

Diff for: src/compiler/core.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1409,9 +1409,12 @@ namespace ts {
14091409
/**
14101410
* Tests whether a value is string
14111411
*/
1412-
export function isString(text: any): text is string {
1412+
export function isString(text: unknown): text is string {
14131413
return typeof text === "string";
14141414
}
1415+
export function isNumber(x: unknown): x is number {
1416+
return typeof x === "number";
1417+
}
14151418

14161419
export function tryCast<TOut extends TIn, TIn = any>(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut | undefined;
14171420
export function tryCast<T>(value: T, test: (value: T) => boolean): T | undefined;
@@ -1534,6 +1537,7 @@ namespace ts {
15341537
* Every function should be assignable to this, but this should not be assignable to every function.
15351538
*/
15361539
export type AnyFunction = (...args: never[]) => void;
1540+
export type AnyConstructor = new (...args: unknown[]) => unknown;
15371541

15381542
export namespace Debug {
15391543
export let currentAssertionLevel = AssertionLevel.None;
@@ -2125,4 +2129,8 @@ namespace ts {
21252129
deleted(oldItems[oldIndex++]);
21262130
}
21272131
}
2132+
2133+
export function fill<T>(length: number, cb: (index: number) => T): T[] {
2134+
return new Array(length).fill(0).map((_, i) => cb(i));
2135+
}
21282136
}

Diff for: src/compiler/diagnosticMessages.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -4671,5 +4671,13 @@
46714671
"Convert all to async functions": {
46724672
"category": "Message",
46734673
"code": 95066
4674+
},
4675+
"Generate types for '{0}'": {
4676+
"category": "Message",
4677+
"code": 95067
4678+
},
4679+
"Generate types for all packages without types": {
4680+
"category": "Message",
4681+
"code": 95068
46744682
}
4675-
}
4683+
}

Diff for: src/compiler/factory.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ namespace ts {
235235

236236
// Modifiers
237237

238-
export function createModifier<T extends Modifier["kind"]>(kind: T) {
238+
export function createModifier<T extends Modifier["kind"]>(kind: T): Token<T> {
239239
return createToken(kind);
240240
}
241241

Diff for: src/compiler/inspectValue.ts

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/* @internal */
2+
namespace ts {
3+
export interface InspectValueOptions {
4+
readonly fileNameToRequire: string;
5+
}
6+
7+
export const enum ValueKind { Const, Array, FunctionOrClass, Object }
8+
export interface ValueInfoBase {
9+
readonly name: string;
10+
}
11+
export type ValueInfo = ValueInfoSimple | ValueInfoArray | ValueInfoFunctionOrClass | ValueInfoObject;
12+
export interface ValueInfoSimple extends ValueInfoBase {
13+
readonly kind: ValueKind.Const;
14+
readonly typeName: string;
15+
readonly comment?: string | undefined;
16+
}
17+
export interface ValueInfoFunctionOrClass extends ValueInfoBase {
18+
readonly kind: ValueKind.FunctionOrClass;
19+
readonly source: string | number; // For a native function, this is the length.
20+
readonly prototypeMembers: ReadonlyArray<ValueInfo>;
21+
readonly namespaceMembers: ReadonlyArray<ValueInfo>;
22+
}
23+
export interface ValueInfoArray extends ValueInfoBase {
24+
readonly kind: ValueKind.Array;
25+
readonly inner: ValueInfo;
26+
}
27+
export interface ValueInfoObject extends ValueInfoBase {
28+
readonly kind: ValueKind.Object;
29+
readonly members: ReadonlyArray<ValueInfo>;
30+
}
31+
32+
export function inspectModule(fileNameToRequire: string): ValueInfo {
33+
return inspectValue(removeFileExtension(getBaseFileName(fileNameToRequire)), tryRequire(fileNameToRequire));
34+
}
35+
36+
export function inspectValue(name: string, value: unknown): ValueInfo {
37+
return getValueInfo(name, value, getRecurser());
38+
}
39+
40+
type Recurser = <T>(obj: unknown, name: string, cbOk: () => T, cbFail: (isCircularReference: boolean, keyStack: ReadonlyArray<string>) => T) => T;
41+
function getRecurser(): Recurser {
42+
const seen = new Set<unknown>();
43+
const nameStack: string[] = [];
44+
return (obj, name, cbOk, cbFail) => {
45+
if (seen.has(obj) || nameStack.length > 4) {
46+
return cbFail(seen.has(obj), nameStack);
47+
}
48+
49+
seen.add(obj);
50+
nameStack.push(name);
51+
const res = cbOk();
52+
nameStack.pop();
53+
seen.delete(obj);
54+
return res;
55+
};
56+
}
57+
58+
function getValueInfo(name: string, value: unknown, recurser: Recurser): ValueInfo {
59+
return recurser(value, name,
60+
(): ValueInfo => {
61+
if (typeof value === "function") return getFunctionOrClassInfo(value as AnyFunction, name, recurser);
62+
if (typeof value === "object") {
63+
const builtin = getBuiltinType(name, value as object, recurser);
64+
if (builtin !== undefined) return builtin;
65+
const entries = getEntriesOfObject(value as object);
66+
return { kind: ValueKind.Object, name, members: flatMap(entries, ({ key, value }) => getValueInfo(key, value, recurser)) };
67+
}
68+
return { kind: ValueKind.Const, name, typeName: isNullOrUndefined(value) ? "any" : typeof value };
69+
},
70+
(isCircularReference, keyStack) => anyValue(name, ` ${isCircularReference ? "Circular reference" : "Too-deep object hierarchy"} from ${keyStack.join(".")}`));
71+
}
72+
73+
function getFunctionOrClassInfo(fn: AnyFunction, name: string, recurser: Recurser): ValueInfoFunctionOrClass {
74+
const prototypeMembers = getPrototypeMembers(fn, recurser);
75+
const namespaceMembers = flatMap(getEntriesOfObject(fn), ({ key, value }) => getValueInfo(key, value, recurser));
76+
const toString = cast(Function.prototype.toString.call(fn), isString);
77+
const source = stringContains(toString, "{ [native code] }") ? getFunctionLength(fn) : toString;
78+
return { kind: ValueKind.FunctionOrClass, name, source, namespaceMembers, prototypeMembers };
79+
}
80+
81+
const builtins: () => ReadonlyMap<AnyConstructor> = memoize(() => {
82+
const map = createMap<AnyConstructor>();
83+
for (const { key, value } of getEntriesOfObject(global)) {
84+
if (typeof value === "function" && typeof value.prototype === "object" && value !== Object) {
85+
map.set(key, value as AnyConstructor);
86+
}
87+
}
88+
return map;
89+
});
90+
function getBuiltinType(name: string, value: object, recurser: Recurser): ValueInfo | undefined {
91+
return isArray(value)
92+
? { name, kind: ValueKind.Array, inner: value.length && getValueInfo("element", first(value), recurser) || anyValue(name) }
93+
: forEachEntry(builtins(), (builtin, builtinName): ValueInfo | undefined =>
94+
value instanceof builtin ? { kind: ValueKind.Const, name, typeName: builtinName } : undefined);
95+
}
96+
97+
function getPrototypeMembers(fn: AnyFunction, recurser: Recurser): ReadonlyArray<ValueInfo> {
98+
const prototype = fn.prototype as unknown;
99+
// tslint:disable-next-line no-unnecessary-type-assertion (TODO: update LKG and it will really be unnecessary)
100+
return typeof prototype !== "object" || prototype === null ? emptyArray : mapDefined(getEntriesOfObject(prototype as object), ({ key, value }) =>
101+
key === "constructor" ? undefined : getValueInfo(key, value, recurser));
102+
}
103+
104+
const ignoredProperties: ReadonlySet<string> = new Set(["arguments", "caller", "constructor", "eval", "super_"]);
105+
const reservedFunctionProperties: ReadonlySet<string> = new Set(Object.getOwnPropertyNames(noop));
106+
interface ObjectEntry { readonly key: string; readonly value: unknown; }
107+
function getEntriesOfObject(obj: object): ReadonlyArray<ObjectEntry> {
108+
const seen = createMap<true>();
109+
const entries: ObjectEntry[] = [];
110+
let chain = obj;
111+
while (!isNullOrUndefined(chain) && chain !== Object.prototype && chain !== Function.prototype) {
112+
for (const key of Object.getOwnPropertyNames(chain)) {
113+
if (!isJsPrivate(key) &&
114+
!ignoredProperties.has(key) &&
115+
(typeof obj !== "function" || !reservedFunctionProperties.has(key)) &&
116+
// Don't add property from a higher prototype if it already exists in a lower one
117+
addToSeen(seen, key)) {
118+
const value = safeGetPropertyOfObject(chain, key);
119+
// Don't repeat "toString" that matches signature from Object.prototype
120+
if (!(key === "toString" && typeof value === "function" && value.length === 0)) {
121+
entries.push({ key, value });
122+
}
123+
}
124+
}
125+
chain = Object.getPrototypeOf(chain);
126+
}
127+
return entries.sort((e1, e2) => compareStringsCaseSensitive(e1.key, e2.key));
128+
}
129+
130+
function getFunctionLength(fn: AnyFunction): number {
131+
return tryCast(safeGetPropertyOfObject(fn, "length"), isNumber) || 0;
132+
}
133+
134+
function safeGetPropertyOfObject(obj: object, key: string): unknown {
135+
const desc = Object.getOwnPropertyDescriptor(obj, key);
136+
return desc && desc.value;
137+
}
138+
139+
function isNullOrUndefined(value: unknown): value is null | undefined {
140+
return value == null; // tslint:disable-line
141+
}
142+
143+
function anyValue(name: string, comment?: string): ValueInfo {
144+
return { kind: ValueKind.Const, name, typeName: "any", comment };
145+
}
146+
147+
export function isJsPrivate(name: string): boolean {
148+
return name.startsWith("_");
149+
}
150+
151+
function tryRequire(fileNameToRequire: string): unknown {
152+
try {
153+
return require(fileNameToRequire);
154+
}
155+
catch {
156+
return undefined;
157+
}
158+
}
159+
}

Diff for: src/compiler/moduleNameResolver.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -779,14 +779,23 @@ namespace ts {
779779
*/
780780
/* @internal */
781781
export function resolveJSModule(moduleName: string, initialDir: string, host: ModuleResolutionHost): string {
782-
const { resolvedModule, failedLookupLocations } =
783-
nodeModuleNameResolverWorker(moduleName, initialDir, { moduleResolution: ModuleResolutionKind.NodeJs, allowJs: true }, host, /*cache*/ undefined, /*jsOnly*/ true);
782+
const { resolvedModule, failedLookupLocations } = tryResolveJSModuleWorker(moduleName, initialDir, host);
784783
if (!resolvedModule) {
785784
throw new Error(`Could not resolve JS module '${moduleName}' starting at '${initialDir}'. Looked in: ${failedLookupLocations.join(", ")}`);
786785
}
787786
return resolvedModule.resolvedFileName;
788787
}
789788

789+
/* @internal */
790+
export function tryResolveJSModule(moduleName: string, initialDir: string, host: ModuleResolutionHost): string | undefined {
791+
const { resolvedModule } = tryResolveJSModuleWorker(moduleName, initialDir, host);
792+
return resolvedModule && resolvedModule.resolvedFileName;
793+
}
794+
795+
function tryResolveJSModuleWorker(moduleName: string, initialDir: string, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
796+
return nodeModuleNameResolverWorker(moduleName, initialDir, { moduleResolution: ModuleResolutionKind.NodeJs, allowJs: true }, host, /*cache*/ undefined, /*jsOnly*/ true);
797+
}
798+
790799
export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations {
791800
return nodeModuleNameResolverWorker(moduleName, getDirectoryPath(containingFile), compilerOptions, host, cache, /*jsOnly*/ false);
792801
}

Diff for: src/compiler/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"resolutionCache.ts",
5353
"moduleSpecifiers.ts",
5454
"watch.ts",
55-
"tsbuild.ts"
55+
"tsbuild.ts",
56+
"inspectValue.ts",
5657
]
5758
}

Diff for: src/harness/fourslash.ts

+28-5
Original file line numberDiff line numberDiff line change
@@ -2502,9 +2502,7 @@ Actual: ${stringify(fullActual)}`);
25022502

25032503
const { changes, commands } = this.languageService.getCombinedCodeFix({ type: "file", fileName: this.activeFile.fileName }, fixId, this.formatCodeSettings, ts.emptyOptions);
25042504
assert.deepEqual<ReadonlyArray<{}> | undefined>(commands, expectedCommands);
2505-
assert(changes.every(c => c.fileName === this.activeFile.fileName), "TODO: support testing codefixes that touch multiple files");
2506-
this.applyChanges(changes);
2507-
this.verifyCurrentFileContent(newFileContent);
2505+
this.verifyNewContent({ newFileContent }, changes);
25082506
}
25092507

25102508
/**
@@ -3389,6 +3387,19 @@ Actual: ${stringify(fullActual)}`);
33893387
private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions): ReadonlyArray<ts.ApplicableRefactorInfo> {
33903388
return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences) || ts.emptyArray;
33913389
}
3390+
3391+
public generateTypes(examples: ReadonlyArray<FourSlashInterface.GenerateTypesOptions>): void {
3392+
for (const { name = "example", value, output, outputBaseline } of examples) {
3393+
const actual = ts.generateTypesForModule(name, value, this.formatCodeSettings);
3394+
if (outputBaseline) {
3395+
if (actual === undefined) throw ts.Debug.fail();
3396+
Harness.Baseline.runBaseline(ts.combinePaths("generateTypes", outputBaseline + ts.Extension.Dts), actual);
3397+
}
3398+
else {
3399+
assert.equal(actual, output, `generateTypes output for ${name} does not match`);
3400+
}
3401+
}
3402+
}
33923403
}
33933404

33943405
function updateTextRangeForTextChanges({ pos, end }: ts.TextRange, textChanges: ReadonlyArray<ts.TextChange>): ts.TextRange {
@@ -3442,7 +3453,7 @@ Actual: ${stringify(fullActual)}`);
34423453
// Parse out the files and their metadata
34433454
const testData = parseTestData(absoluteBasePath, content, absoluteFileName);
34443455
const state = new TestState(absoluteBasePath, testType, testData);
3445-
const output = ts.transpileModule(content, { reportDiagnostics: true });
3456+
const output = ts.transpileModule(content, { reportDiagnostics: true, compilerOptions: { target: ts.ScriptTarget.ES2015 } });
34463457
if (output.diagnostics!.length > 0) {
34473458
throw new Error(`Syntax error in ${absoluteBasePath}: ${output.diagnostics![0].messageText}`);
34483459
}
@@ -4512,6 +4523,18 @@ namespace FourSlashInterface {
45124523
public noMoveToNewFile(): void {
45134524
this.state.noMoveToNewFile();
45144525
}
4526+
4527+
public generateTypes(...options: GenerateTypesOptions[]): void {
4528+
this.state.generateTypes(options);
4529+
}
4530+
}
4531+
4532+
export interface GenerateTypesOptions {
4533+
readonly name?: string;
4534+
readonly value: unknown;
4535+
// Exactly one of these should be set:
4536+
readonly output?: string;
4537+
readonly outputBaseline?: string;
45154538
}
45164539

45174540
export class Edit {
@@ -4901,7 +4924,7 @@ namespace FourSlashInterface {
49014924
export interface VerifyCodeFixAllOptions {
49024925
fixId: string;
49034926
fixAllDescription: string;
4904-
newFileContent: string;
4927+
newFileContent: NewFileContent;
49054928
commands: ReadonlyArray<{}>;
49064929
}
49074930

Diff for: src/jsTyping/shared.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace ts.server {
44
export const ActionSet: ActionSet = "action::set";
55
export const ActionInvalidate: ActionInvalidate = "action::invalidate";
66
export const ActionPackageInstalled: ActionPackageInstalled = "action::packageInstalled";
7+
export const ActionValueInspected: ActionValueInspected = "action::valueInspected";
78
export const EventTypesRegistry: EventTypesRegistry = "event::typesRegistry";
89
export const EventBeginInstallTypes: EventBeginInstallTypes = "event::beginInstallTypes";
910
export const EventEndInstallTypes: EventEndInstallTypes = "event::endInstallTypes";

Diff for: src/jsTyping/types.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ declare namespace ts.server {
22
export type ActionSet = "action::set";
33
export type ActionInvalidate = "action::invalidate";
44
export type ActionPackageInstalled = "action::packageInstalled";
5+
export type ActionValueInspected = "action::valueInspected";
56
export type EventTypesRegistry = "event::typesRegistry";
67
export type EventBeginInstallTypes = "event::beginInstallTypes";
78
export type EventEndInstallTypes = "event::endInstallTypes";
@@ -12,15 +13,15 @@ declare namespace ts.server {
1213
}
1314

1415
export interface TypingInstallerResponse {
15-
readonly kind: ActionSet | ActionInvalidate | EventTypesRegistry | ActionPackageInstalled | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed;
16+
readonly kind: ActionSet | ActionInvalidate | EventTypesRegistry | ActionPackageInstalled | ActionValueInspected | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed;
1617
}
1718

1819
export interface TypingInstallerRequestWithProjectName {
1920
readonly projectName: string;
2021
}
2122

2223
/* @internal */
23-
export type TypingInstallerRequestUnion = DiscoverTypings | CloseProject | TypesRegistryRequest | InstallPackageRequest;
24+
export type TypingInstallerRequestUnion = DiscoverTypings | CloseProject | TypesRegistryRequest | InstallPackageRequest | InspectValueRequest;
2425

2526
export interface DiscoverTypings extends TypingInstallerRequestWithProjectName {
2627
readonly fileNames: string[];
@@ -47,6 +48,12 @@ declare namespace ts.server {
4748
readonly projectRootPath: Path;
4849
}
4950

51+
/* @internal */
52+
export interface InspectValueRequest {
53+
readonly kind: "inspectValue";
54+
readonly options: InspectValueOptions;
55+
}
56+
5057
/* @internal */
5158
export interface TypesRegistryResponse extends TypingInstallerResponse {
5259
readonly kind: EventTypesRegistry;
@@ -59,6 +66,12 @@ declare namespace ts.server {
5966
readonly message: string;
6067
}
6168

69+
/* @internal */
70+
export interface InspectValueResponse {
71+
readonly kind: ActionValueInspected;
72+
readonly result: ValueInfo;
73+
}
74+
6275
export interface InitializationFailedResponse extends TypingInstallerResponse {
6376
readonly kind: EventInitializationFailed;
6477
readonly message: string;
@@ -106,5 +119,5 @@ declare namespace ts.server {
106119
}
107120

108121
/* @internal */
109-
export type TypingInstallerResponseUnion = SetTypings | InvalidateCachedTypings | TypesRegistryResponse | PackageInstalledResponse | InstallTypes | InitializationFailedResponse;
122+
export type TypingInstallerResponseUnion = SetTypings | InvalidateCachedTypings | TypesRegistryResponse | PackageInstalledResponse | InspectValueResponse | InstallTypes | InitializationFailedResponse;
110123
}

0 commit comments

Comments
 (0)