Skip to content

Commit a94774a

Browse files
author
Andy Hanson
committed
Add codefix to generate types for untyped module
1 parent 194ffb3 commit a94774a

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

+2120
-119
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
@@ -1414,9 +1414,12 @@ namespace ts {
14141414
/**
14151415
* Tests whether a value is string
14161416
*/
1417-
export function isString(text: any): text is string {
1417+
export function isString(text: unknown): text is string {
14181418
return typeof text === "string";
14191419
}
1420+
export function isNumber(x: unknown): x is number {
1421+
return typeof x === "number";
1422+
}
14201423

14211424
export function tryCast<TOut extends TIn, TIn = any>(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut | undefined;
14221425
export function tryCast<T>(value: T, test: (value: T) => boolean): T | undefined;
@@ -1539,6 +1542,7 @@ namespace ts {
15391542
* Every function should be assignable to this, but this should not be assignable to every function.
15401543
*/
15411544
export type AnyFunction = (...args: never[]) => void;
1545+
export type AnyConstructor = new (...args: unknown[]) => unknown;
15421546

15431547
export namespace Debug {
15441548
export let currentAssertionLevel = AssertionLevel.None;
@@ -2130,4 +2134,8 @@ namespace ts {
21302134
deleted(oldItems[oldIndex++]);
21312135
}
21322136
}
2137+
2138+
export function fill<T>(length: number, cb: (index: number) => T): T[] {
2139+
return new Array(length).fill(0).map((_, i) => cb(i));
2140+
}
21332141
}

Diff for: src/compiler/diagnosticMessages.json

+12-4
Original file line numberDiff line numberDiff line change
@@ -4559,7 +4559,7 @@
45594559
"category": "Message",
45604560
"code": 95062
45614561
},
4562-
4562+
45634563
"Add missing enum member '{0}'": {
45644564
"category": "Message",
45654565
"code": 95063
@@ -4573,7 +4573,15 @@
45734573
"code": 95065
45744574
},
45754575
"Convert all to async functions": {
4576-
"category": "Message",
4577-
"code": 95066
4576+
"category": "Message",
4577+
"code": 95066
4578+
},
4579+
"Generate types for '{0}'": {
4580+
"category": "Message",
4581+
"code": 95067
4582+
},
4583+
"Generate types for all packages without types": {
4584+
"category": "Message",
4585+
"code": 95068
45784586
}
4579-
}
4587+
}

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

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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+
return typeof prototype !== "object" || prototype === null ? emptyArray : mapDefined(getEntriesOfObject(prototype as object), ({ key, value }) =>
100+
key === "constructor" ? undefined : getValueInfo(key, value, recurser));
101+
}
102+
103+
const ignoredProperties: ReadonlySet<string> = new Set(["arguments", "caller", "constructor", "eval", "super_"]);
104+
const reservedFunctionProperties: ReadonlySet<string> = new Set(Object.getOwnPropertyNames(noop));
105+
interface ObjectEntry { readonly key: string; readonly value: unknown; }
106+
function getEntriesOfObject(obj: object): ReadonlyArray<ObjectEntry> {
107+
const seen = createMap<true>();
108+
const entries: ObjectEntry[] = [];
109+
let chain = obj;
110+
while (!isNullOrUndefined(chain) && chain !== Object.prototype && chain !== Function.prototype) {
111+
for (const key of Object.getOwnPropertyNames(chain)) {
112+
if (!isJsPrivate(key) &&
113+
!ignoredProperties.has(key) &&
114+
(typeof obj !== "function" || !reservedFunctionProperties.has(key)) &&
115+
// Don't add property from a higher prototype if it already exists in a lower one
116+
addToSeen(seen, key)) {
117+
const value = safeGetPropertyOfObject(chain, key);
118+
// Don't repeat "toString" that matches signature from Object.prototype
119+
if (!(key === "toString" && typeof value === "function" && value.length === 0)) {
120+
entries.push({ key, value });
121+
}
122+
}
123+
}
124+
chain = Object.getPrototypeOf(chain);
125+
}
126+
return entries.sort((e1, e2) => compareStringsCaseSensitive(e1.key, e2.key));
127+
}
128+
129+
function getFunctionLength(fn: AnyFunction): number {
130+
return tryCast(safeGetPropertyOfObject(fn, "length"), isNumber) || 0;
131+
}
132+
133+
function safeGetPropertyOfObject(obj: object, key: string): unknown {
134+
const desc = Object.getOwnPropertyDescriptor(obj, key);
135+
return desc && desc.value;
136+
}
137+
138+
function isNullOrUndefined(value: unknown): value is null | undefined {
139+
return value == null; // tslint:disable-line
140+
}
141+
142+
function anyValue(name: string, comment?: string): ValueInfo {
143+
return { kind: ValueKind.Const, name, typeName: "any", comment };
144+
}
145+
146+
export function isJsPrivate(name: string): boolean {
147+
return name.startsWith("_");
148+
}
149+
150+
function tryRequire(fileNameToRequire: string): unknown {
151+
try {
152+
return require(fileNameToRequire);
153+
}
154+
catch {
155+
return undefined;
156+
}
157+
}
158+
}

Diff for: src/compiler/moduleNameResolver.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -740,14 +740,23 @@ namespace ts {
740740
*/
741741
/* @internal */
742742
export function resolveJavaScriptModule(moduleName: string, initialDir: string, host: ModuleResolutionHost): string {
743-
const { resolvedModule, failedLookupLocations } =
744-
nodeModuleNameResolverWorker(moduleName, initialDir, { moduleResolution: ModuleResolutionKind.NodeJs, allowJs: true }, host, /*cache*/ undefined, /*jsOnly*/ true);
743+
const { resolvedModule, failedLookupLocations } = tryResolveJavaScriptModuleWorker(moduleName, initialDir, host);
745744
if (!resolvedModule) {
746745
throw new Error(`Could not resolve JS module '${moduleName}' starting at '${initialDir}'. Looked in: ${failedLookupLocations.join(", ")}`);
747746
}
748747
return resolvedModule.resolvedFileName;
749748
}
750749

750+
/* @internal */
751+
export function tryResolveJavaScriptModule(moduleName: string, initialDir: string, host: ModuleResolutionHost): string | undefined {
752+
const { resolvedModule } = tryResolveJavaScriptModuleWorker(moduleName, initialDir, host);
753+
return resolvedModule && resolvedModule.resolvedFileName;
754+
}
755+
756+
function tryResolveJavaScriptModuleWorker(moduleName: string, initialDir: string, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
757+
return nodeModuleNameResolverWorker(moduleName, initialDir, { moduleResolution: ModuleResolutionKind.NodeJs, allowJs: true }, host, /*cache*/ undefined, /*jsOnly*/ true);
758+
}
759+
751760
function nodeModuleNameResolverWorker(moduleName: string, containingDirectory: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache: ModuleResolutionCache | undefined, jsOnly: boolean): ResolvedModuleWithFailedLookupLocations {
752761
const traceEnabled = isTraceEnabled(compilerOptions, host);
753762

Diff for: src/compiler/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"resolutionCache.ts",
5252
"moduleSpecifiers.ts",
5353
"watch.ts",
54-
"tsbuild.ts"
54+
"tsbuild.ts",
55+
"inspectValue.ts",
5556
]
5657
}

Diff for: src/harness/fourslash.ts

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

24942494
const { changes, commands } = this.languageService.getCombinedCodeFix({ type: "file", fileName: this.activeFile.fileName }, fixId, this.formatCodeSettings, ts.emptyOptions);
24952495
assert.deepEqual<ReadonlyArray<{}> | undefined>(commands, expectedCommands);
2496-
assert(changes.every(c => c.fileName === this.activeFile.fileName), "TODO: support testing codefixes that touch multiple files");
2497-
this.applyChanges(changes);
2498-
this.verifyCurrentFileContent(newFileContent);
2496+
this.verifyNewContent({ newFileContent }, changes);
24992497
}
25002498

25012499
/**
@@ -3380,6 +3378,19 @@ Actual: ${stringify(fullActual)}`);
33803378
private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions): ReadonlyArray<ts.ApplicableRefactorInfo> {
33813379
return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences) || ts.emptyArray;
33823380
}
3381+
3382+
public generateTypes(examples: ReadonlyArray<FourSlashInterface.GenerateTypesOptions>): void {
3383+
for (const { name = "example", value, output, outputBaseline } of examples) {
3384+
const actual = ts.generateTypesForModule(name, value, this.formatCodeSettings);
3385+
if (outputBaseline) {
3386+
if (actual === undefined) throw ts.Debug.fail();
3387+
Harness.Baseline.runBaseline(ts.combinePaths("generateTypes", outputBaseline + ts.Extension.Dts), actual);
3388+
}
3389+
else {
3390+
assert.equal(actual, output, `generateTypes output for ${name} does not match`);
3391+
}
3392+
}
3393+
}
33833394
}
33843395

33853396
function updateTextRangeForTextChanges({ pos, end }: ts.TextRange, textChanges: ReadonlyArray<ts.TextChange>): ts.TextRange {
@@ -3433,7 +3444,7 @@ Actual: ${stringify(fullActual)}`);
34333444
// Parse out the files and their metadata
34343445
const testData = parseTestData(absoluteBasePath, content, absoluteFileName);
34353446
const state = new TestState(absoluteBasePath, testType, testData);
3436-
const output = ts.transpileModule(content, { reportDiagnostics: true });
3447+
const output = ts.transpileModule(content, { reportDiagnostics: true, compilerOptions: { target: ts.ScriptTarget.ES2015 } });
34373448
if (output.diagnostics!.length > 0) {
34383449
throw new Error(`Syntax error in ${absoluteBasePath}: ${output.diagnostics![0].messageText}`);
34393450
}
@@ -4503,6 +4514,18 @@ namespace FourSlashInterface {
45034514
public noMoveToNewFile(): void {
45044515
this.state.noMoveToNewFile();
45054516
}
4517+
4518+
public generateTypes(...options: GenerateTypesOptions[]): void {
4519+
this.state.generateTypes(options);
4520+
}
4521+
}
4522+
4523+
export interface GenerateTypesOptions {
4524+
readonly name?: string;
4525+
readonly value: unknown;
4526+
// Exactly one of these should be set:
4527+
readonly output?: string;
4528+
readonly outputBaseline?: string;
45064529
}
45074530

45084531
export class Edit {
@@ -4890,7 +4913,7 @@ namespace FourSlashInterface {
48904913
export interface VerifyCodeFixAllOptions {
48914914
fixId: string;
48924915
fixAllDescription: string;
4893-
newFileContent: string;
4916+
newFileContent: NewFileContent;
48944917
commands: ReadonlyArray<{}>;
48954918
}
48964919

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";

0 commit comments

Comments
 (0)