Skip to content

Add codefix to generate types for untyped module #26588

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
4 commits merged into from
Sep 18, 2018
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"gulp-typescript": "latest",
"istanbul": "latest",
"jake": "latest",
"lodash": "4.17.10",
"merge2": "latest",
"minimist": "latest",
"mkdirp": "latest",
Expand Down
10 changes: 9 additions & 1 deletion src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1409,9 +1409,12 @@ namespace ts {
/**
* Tests whether a value is string
*/
export function isString(text: any): text is string {
export function isString(text: unknown): text is string {
return typeof text === "string";
}
export function isNumber(x: unknown): x is number {
return typeof x === "number";
}

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

export namespace Debug {
export let currentAssertionLevel = AssertionLevel.None;
Expand Down Expand Up @@ -2125,4 +2129,8 @@ namespace ts {
deleted(oldItems[oldIndex++]);
}
}

export function fill<T>(length: number, cb: (index: number) => T): T[] {
return new Array(length).fill(0).map((_, i) => cb(i));
}
}
10 changes: 9 additions & 1 deletion src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -4671,5 +4671,13 @@
"Convert all to async functions": {
"category": "Message",
"code": 95066
},
"Generate types for '{0}'": {
"category": "Message",
"code": 95067
},
"Generate types for all packages without types": {
"category": "Message",
"code": 95068
}
}
}
2 changes: 1 addition & 1 deletion src/compiler/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ namespace ts {

// Modifiers

export function createModifier<T extends Modifier["kind"]>(kind: T) {
export function createModifier<T extends Modifier["kind"]>(kind: T): Token<T> {
return createToken(kind);
}

Expand Down
159 changes: 159 additions & 0 deletions src/compiler/inspectValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/* @internal */
namespace ts {
export interface InspectValueOptions {
readonly fileNameToRequire: string;
}

export const enum ValueKind { Const, Array, FunctionOrClass, Object }
export interface ValueInfoBase {
readonly name: string;
}
export type ValueInfo = ValueInfoSimple | ValueInfoArray | ValueInfoFunctionOrClass | ValueInfoObject;
export interface ValueInfoSimple extends ValueInfoBase {
readonly kind: ValueKind.Const;
readonly typeName: string;
readonly comment?: string | undefined;
}
export interface ValueInfoFunctionOrClass extends ValueInfoBase {
readonly kind: ValueKind.FunctionOrClass;
readonly source: string | number; // For a native function, this is the length.
readonly prototypeMembers: ReadonlyArray<ValueInfo>;
readonly namespaceMembers: ReadonlyArray<ValueInfo>;
}
export interface ValueInfoArray extends ValueInfoBase {
readonly kind: ValueKind.Array;
readonly inner: ValueInfo;
}
export interface ValueInfoObject extends ValueInfoBase {
readonly kind: ValueKind.Object;
readonly members: ReadonlyArray<ValueInfo>;
}

export function inspectModule(fileNameToRequire: string): ValueInfo {
return inspectValue(removeFileExtension(getBaseFileName(fileNameToRequire)), tryRequire(fileNameToRequire));
}

export function inspectValue(name: string, value: unknown): ValueInfo {
return getValueInfo(name, value, getRecurser());
}

type Recurser = <T>(obj: unknown, name: string, cbOk: () => T, cbFail: (isCircularReference: boolean, keyStack: ReadonlyArray<string>) => T) => T;
function getRecurser(): Recurser {
const seen = new Set<unknown>();
const nameStack: string[] = [];
return (obj, name, cbOk, cbFail) => {
if (seen.has(obj) || nameStack.length > 4) {
return cbFail(seen.has(obj), nameStack);
}

seen.add(obj);
nameStack.push(name);
const res = cbOk();
nameStack.pop();
seen.delete(obj);
return res;
};
}

function getValueInfo(name: string, value: unknown, recurser: Recurser): ValueInfo {
return recurser(value, name,
(): ValueInfo => {
if (typeof value === "function") return getFunctionOrClassInfo(value as AnyFunction, name, recurser);
if (typeof value === "object") {
const builtin = getBuiltinType(name, value as object, recurser);
if (builtin !== undefined) return builtin;
const entries = getEntriesOfObject(value as object);
return { kind: ValueKind.Object, name, members: flatMap(entries, ({ key, value }) => getValueInfo(key, value, recurser)) };
}
return { kind: ValueKind.Const, name, typeName: isNullOrUndefined(value) ? "any" : typeof value };
},
(isCircularReference, keyStack) => anyValue(name, ` ${isCircularReference ? "Circular reference" : "Too-deep object hierarchy"} from ${keyStack.join(".")}`));
}

function getFunctionOrClassInfo(fn: AnyFunction, name: string, recurser: Recurser): ValueInfoFunctionOrClass {
const prototypeMembers = getPrototypeMembers(fn, recurser);
const namespaceMembers = flatMap(getEntriesOfObject(fn), ({ key, value }) => getValueInfo(key, value, recurser));
const toString = cast(Function.prototype.toString.call(fn), isString);
const source = stringContains(toString, "{ [native code] }") ? getFunctionLength(fn) : toString;
return { kind: ValueKind.FunctionOrClass, name, source, namespaceMembers, prototypeMembers };
}

const builtins: () => ReadonlyMap<AnyConstructor> = memoize(() => {
const map = createMap<AnyConstructor>();
for (const { key, value } of getEntriesOfObject(global)) {
if (typeof value === "function" && typeof value.prototype === "object" && value !== Object) {
map.set(key, value as AnyConstructor);
}
}
return map;
});
function getBuiltinType(name: string, value: object, recurser: Recurser): ValueInfo | undefined {
return isArray(value)
? { name, kind: ValueKind.Array, inner: value.length && getValueInfo("element", first(value), recurser) || anyValue(name) }
: forEachEntry(builtins(), (builtin, builtinName): ValueInfo | undefined =>
value instanceof builtin ? { kind: ValueKind.Const, name, typeName: builtinName } : undefined);
}

function getPrototypeMembers(fn: AnyFunction, recurser: Recurser): ReadonlyArray<ValueInfo> {
const prototype = fn.prototype as unknown;
// tslint:disable-next-line no-unnecessary-type-assertion (TODO: update LKG and it will really be unnecessary)
return typeof prototype !== "object" || prototype === null ? emptyArray : mapDefined(getEntriesOfObject(prototype as object), ({ key, value }) =>
key === "constructor" ? undefined : getValueInfo(key, value, recurser));
}

const ignoredProperties: ReadonlySet<string> = new Set(["arguments", "caller", "constructor", "eval", "super_"]);
const reservedFunctionProperties: ReadonlySet<string> = new Set(Object.getOwnPropertyNames(noop));
interface ObjectEntry { readonly key: string; readonly value: unknown; }
function getEntriesOfObject(obj: object): ReadonlyArray<ObjectEntry> {
const seen = createMap<true>();
const entries: ObjectEntry[] = [];
let chain = obj;
while (!isNullOrUndefined(chain) && chain !== Object.prototype && chain !== Function.prototype) {
for (const key of Object.getOwnPropertyNames(chain)) {
if (!isJsPrivate(key) &&
!ignoredProperties.has(key) &&
(typeof obj !== "function" || !reservedFunctionProperties.has(key)) &&
// Don't add property from a higher prototype if it already exists in a lower one
addToSeen(seen, key)) {
const value = safeGetPropertyOfObject(chain, key);
// Don't repeat "toString" that matches signature from Object.prototype
if (!(key === "toString" && typeof value === "function" && value.length === 0)) {
entries.push({ key, value });
}
}
}
chain = Object.getPrototypeOf(chain);
}
return entries.sort((e1, e2) => compareStringsCaseSensitive(e1.key, e2.key));
}

function getFunctionLength(fn: AnyFunction): number {
return tryCast(safeGetPropertyOfObject(fn, "length"), isNumber) || 0;
}

function safeGetPropertyOfObject(obj: object, key: string): unknown {
const desc = Object.getOwnPropertyDescriptor(obj, key);
return desc && desc.value;
}

function isNullOrUndefined(value: unknown): value is null | undefined {
return value == null; // tslint:disable-line
}

function anyValue(name: string, comment?: string): ValueInfo {
return { kind: ValueKind.Const, name, typeName: "any", comment };
}

export function isJsPrivate(name: string): boolean {
return name.startsWith("_");
}

function tryRequire(fileNameToRequire: string): unknown {
try {
return require(fileNameToRequire);
}
catch {
return undefined;
}
}
}
13 changes: 11 additions & 2 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -779,14 +779,23 @@ namespace ts {
*/
/* @internal */
export function resolveJSModule(moduleName: string, initialDir: string, host: ModuleResolutionHost): string {
const { resolvedModule, failedLookupLocations } =
nodeModuleNameResolverWorker(moduleName, initialDir, { moduleResolution: ModuleResolutionKind.NodeJs, allowJs: true }, host, /*cache*/ undefined, /*jsOnly*/ true);
const { resolvedModule, failedLookupLocations } = tryResolveJSModuleWorker(moduleName, initialDir, host);
if (!resolvedModule) {
throw new Error(`Could not resolve JS module '${moduleName}' starting at '${initialDir}'. Looked in: ${failedLookupLocations.join(", ")}`);
}
return resolvedModule.resolvedFileName;
}

/* @internal */
export function tryResolveJSModule(moduleName: string, initialDir: string, host: ModuleResolutionHost): string | undefined {
const { resolvedModule } = tryResolveJSModuleWorker(moduleName, initialDir, host);
return resolvedModule && resolvedModule.resolvedFileName;
}

function tryResolveJSModuleWorker(moduleName: string, initialDir: string, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
return nodeModuleNameResolverWorker(moduleName, initialDir, { moduleResolution: ModuleResolutionKind.NodeJs, allowJs: true }, host, /*cache*/ undefined, /*jsOnly*/ true);
}

export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations {
return nodeModuleNameResolverWorker(moduleName, getDirectoryPath(containingFile), compilerOptions, host, cache, /*jsOnly*/ false);
}
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"resolutionCache.ts",
"moduleSpecifiers.ts",
"watch.ts",
"tsbuild.ts"
"tsbuild.ts",
"inspectValue.ts",
]
}
33 changes: 28 additions & 5 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2502,9 +2502,7 @@ Actual: ${stringify(fullActual)}`);

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

/**
Expand Down Expand Up @@ -3389,6 +3387,19 @@ Actual: ${stringify(fullActual)}`);
private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions): ReadonlyArray<ts.ApplicableRefactorInfo> {
return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences) || ts.emptyArray;
}

public generateTypes(examples: ReadonlyArray<FourSlashInterface.GenerateTypesOptions>): void {
for (const { name = "example", value, output, outputBaseline } of examples) {
const actual = ts.generateTypesForModule(name, value, this.formatCodeSettings);
if (outputBaseline) {
if (actual === undefined) throw ts.Debug.fail();
Harness.Baseline.runBaseline(ts.combinePaths("generateTypes", outputBaseline + ts.Extension.Dts), actual);
}
else {
assert.equal(actual, output, `generateTypes output for ${name} does not match`);
}
}
}
}

function updateTextRangeForTextChanges({ pos, end }: ts.TextRange, textChanges: ReadonlyArray<ts.TextChange>): ts.TextRange {
Expand Down Expand Up @@ -3442,7 +3453,7 @@ Actual: ${stringify(fullActual)}`);
// Parse out the files and their metadata
const testData = parseTestData(absoluteBasePath, content, absoluteFileName);
const state = new TestState(absoluteBasePath, testType, testData);
const output = ts.transpileModule(content, { reportDiagnostics: true });
const output = ts.transpileModule(content, { reportDiagnostics: true, compilerOptions: { target: ts.ScriptTarget.ES2015 } });
if (output.diagnostics!.length > 0) {
throw new Error(`Syntax error in ${absoluteBasePath}: ${output.diagnostics![0].messageText}`);
}
Expand Down Expand Up @@ -4512,6 +4523,18 @@ namespace FourSlashInterface {
public noMoveToNewFile(): void {
this.state.noMoveToNewFile();
}

public generateTypes(...options: GenerateTypesOptions[]): void {
this.state.generateTypes(options);
}
}

export interface GenerateTypesOptions {
readonly name?: string;
readonly value: unknown;
// Exactly one of these should be set:
readonly output?: string;
readonly outputBaseline?: string;
}

export class Edit {
Expand Down Expand Up @@ -4901,7 +4924,7 @@ namespace FourSlashInterface {
export interface VerifyCodeFixAllOptions {
fixId: string;
fixAllDescription: string;
newFileContent: string;
newFileContent: NewFileContent;
commands: ReadonlyArray<{}>;
}

Expand Down
1 change: 1 addition & 0 deletions src/jsTyping/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace ts.server {
export const ActionSet: ActionSet = "action::set";
export const ActionInvalidate: ActionInvalidate = "action::invalidate";
export const ActionPackageInstalled: ActionPackageInstalled = "action::packageInstalled";
export const ActionValueInspected: ActionValueInspected = "action::valueInspected";
export const EventTypesRegistry: EventTypesRegistry = "event::typesRegistry";
export const EventBeginInstallTypes: EventBeginInstallTypes = "event::beginInstallTypes";
export const EventEndInstallTypes: EventEndInstallTypes = "event::endInstallTypes";
Expand Down
19 changes: 16 additions & 3 deletions src/jsTyping/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ declare namespace ts.server {
export type ActionSet = "action::set";
export type ActionInvalidate = "action::invalidate";
export type ActionPackageInstalled = "action::packageInstalled";
export type ActionValueInspected = "action::valueInspected";
export type EventTypesRegistry = "event::typesRegistry";
export type EventBeginInstallTypes = "event::beginInstallTypes";
export type EventEndInstallTypes = "event::endInstallTypes";
Expand All @@ -12,15 +13,15 @@ declare namespace ts.server {
}

export interface TypingInstallerResponse {
readonly kind: ActionSet | ActionInvalidate | EventTypesRegistry | ActionPackageInstalled | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed;
readonly kind: ActionSet | ActionInvalidate | EventTypesRegistry | ActionPackageInstalled | ActionValueInspected | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed;
}

export interface TypingInstallerRequestWithProjectName {
readonly projectName: string;
}

/* @internal */
export type TypingInstallerRequestUnion = DiscoverTypings | CloseProject | TypesRegistryRequest | InstallPackageRequest;
export type TypingInstallerRequestUnion = DiscoverTypings | CloseProject | TypesRegistryRequest | InstallPackageRequest | InspectValueRequest;

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

/* @internal */
export interface InspectValueRequest {
readonly kind: "inspectValue";
readonly options: InspectValueOptions;
}

/* @internal */
export interface TypesRegistryResponse extends TypingInstallerResponse {
readonly kind: EventTypesRegistry;
Expand All @@ -59,6 +66,12 @@ declare namespace ts.server {
readonly message: string;
}

/* @internal */
export interface InspectValueResponse {
readonly kind: ActionValueInspected;
readonly result: ValueInfo;
}

export interface InitializationFailedResponse extends TypingInstallerResponse {
readonly kind: EventInitializationFailed;
readonly message: string;
Expand Down Expand Up @@ -106,5 +119,5 @@ declare namespace ts.server {
}

/* @internal */
export type TypingInstallerResponseUnion = SetTypings | InvalidateCachedTypings | TypesRegistryResponse | PackageInstalledResponse | InstallTypes | InitializationFailedResponse;
export type TypingInstallerResponseUnion = SetTypings | InvalidateCachedTypings | TypesRegistryResponse | PackageInstalledResponse | InspectValueResponse | InstallTypes | InitializationFailedResponse;
}
Loading