From c372ac90235fee715a8fb504b944a1426ec9472c Mon Sep 17 00:00:00 2001 From: Orta Therox Date: Fri, 13 Sep 2019 10:51:22 -0400 Subject: [PATCH 1/2] Fix master with 3.6 --- source/lib/compiler.ts | 3 ++- source/lib/interfaces.ts | 3 ++- source/test/test.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/source/lib/compiler.ts b/source/lib/compiler.ts index 04e746a5..d20bc95f 100644 --- a/source/lib/compiler.ts +++ b/source/lib/compiler.ts @@ -22,7 +22,8 @@ const diagnosticCodesToIgnore = new Set([ DiagnosticCode.CannotAssignToReadOnlyProperty, DiagnosticCode.TypeIsNotAssignableToOtherType, DiagnosticCode.GenericTypeRequiresTypeArguments, - DiagnosticCode.ExpectedArgumentsButGotOther + DiagnosticCode.ExpectedArgumentsButGotOther, + DiagnosticCode.NoOverloadMatches ]); /** diff --git a/source/lib/interfaces.ts b/source/lib/interfaces.ts index 686945bc..0dd85430 100644 --- a/source/lib/interfaces.ts +++ b/source/lib/interfaces.ts @@ -26,7 +26,8 @@ export enum DiagnosticCode { PropertyDoesNotExistOnType = 2339, ArgumentTypeIsNotAssignableToParameterType = 2345, CannotAssignToReadOnlyProperty = 2540, - ExpectedArgumentsButGotOther = 2554 + ExpectedArgumentsButGotOther = 2554, + NoOverloadMatches = 2769 } export interface Diagnostic { diff --git a/source/test/test.ts b/source/test/test.ts index f9f164d6..750f6543 100644 --- a/source/test/test.ts +++ b/source/test/test.ts @@ -189,7 +189,7 @@ test('support setting a custom test directory', async t => { test('expectError for functions', async t => { const diagnostics = await m({cwd: path.join(__dirname, 'fixtures/expect-error/functions')}); - t.true(diagnostics.length === 1); + t.true(diagnostics.length === 1, `Diagnostics: ${diagnostics.map(d => d.message)}`); t.true(diagnostics[0].column === 0); t.true(diagnostics[0].line === 5); From 451b99e70046c00468a7858e39937f6aa5b5ad64 Mon Sep 17 00:00:00 2001 From: Orta Therox Date: Fri, 13 Sep 2019 13:22:47 -0400 Subject: [PATCH 2/2] Adds a way to validate your memory usage via dts files --- source/cli.ts | 33 +++++++++++- source/lib/compiler.ts | 51 ++++++++++++------ source/lib/index.ts | 31 +++++++---- source/lib/interfaces.ts | 20 ++++++- source/lib/memory-validator.ts | 85 ++++++++++++++++++++++++++++++ source/lib/rules/files-property.ts | 2 +- source/lib/rules/types-property.ts | 2 +- 7 files changed, 193 insertions(+), 31 deletions(-) create mode 100644 source/lib/memory-validator.ts diff --git a/source/cli.ts b/source/cli.ts index 0e91af56..04435e53 100644 --- a/source/cli.ts +++ b/source/cli.ts @@ -8,6 +8,11 @@ const cli = meow(` Usage $ tsd [path] + Options + --verify-size, -s Keep on top of the memory usage with a data snapshot + --write, -w Overwrite existing memory usage JSON fixture + --size-delta, -d What percent of change is allow in memory usage, default is 10 + Examples $ tsd /path/to/project @@ -15,13 +20,37 @@ const cli = meow(` index.test-d.ts ✖ 10:20 Argument of type string is not assignable to parameter of type number. -`); +`, + { + flags: { + verifySize: { + type: 'boolean', + alias: 'u' + }, + write: { + type: 'boolean', + alias: 'w' + }, + sizeDelta: { + type: 'string', + alias: 'd', + default: '10' + } + } + } +); (async () => { // tslint:disable-line:no-floating-promises updateNotifier({pkg: cli.pkg}).notify(); try { - const options = cli.input.length > 0 ? {cwd: cli.input[0]} : undefined; + const cwd = cli.input.length > 0 ? cli.input[0] : process.cwd(); + const options = { + cwd, + verify: cli.flags.verifySize, + writeSnapshot: cli.flags.write, + sizeDelta: parseInt(cli.flags.sizeDelta, 10) + }; const diagnostics = await tsd(options); diff --git a/source/lib/compiler.ts b/source/lib/compiler.ts index d20bc95f..30a04aa8 100644 --- a/source/lib/compiler.ts +++ b/source/lib/compiler.ts @@ -7,9 +7,10 @@ import { Program, SourceFile, Node, - forEachChild + forEachChild, + sys } from 'typescript'; -import {Diagnostic, DiagnosticCode, Context, Location} from './interfaces'; +import {Diagnostic, DiagnosticCode, Context, Location, RunResults} from './interfaces'; // List of diagnostic codes that should be ignored const ignoredDiagnostics = new Set([ @@ -96,19 +97,15 @@ const ignoreDiagnostic = (diagnostic: TSDiagnostic, expectedErrors: Map { - const fileNames = context.testFiles.map(fileName => path.join(context.cwd, fileName)); - - const result: Diagnostic[] = []; - - const program = createProgram(fileNames, context.config.compilerOptions); +export const getDiagnostics = (program: Program) => { + const results: Diagnostic[] = []; - const diagnostics = program + const diagnostics = program .getSemanticDiagnostics() .concat(program.getSyntacticDiagnostics()); @@ -121,7 +118,7 @@ export const getDiagnostics = (context: Context): Diagnostic[] => { const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start as number); - result.push({ + results.push({ fileName: diagnostic.file.fileName, message: flattenDiagnosticMessageText(diagnostic.messageText, '\n'), severity: 'error', @@ -131,12 +128,36 @@ export const getDiagnostics = (context: Context): Diagnostic[] => { } for (const [, diagnostic] of expectedErrors) { - result.push({ + results.push({ ...diagnostic, message: 'Expected an error, but found none.', severity: 'error' }); } - return result; + return results; +}; + +/** + * Get a list of TypeScript diagnostics, and memory size within the current context. + * + * @param context - The context object. + * @returns An object with memory stats, and diagnostics + */ +export const getTypeScriptResults = (context: Context): RunResults => { + const fileNames = context.testFiles.map(fileName => path.join(context.options.cwd, fileName)); + + const program = createProgram(fileNames, context.config.compilerOptions); + const diagnostics = getDiagnostics(program); + + // These are private as of 3.6, but will be public in 3.7 - microsoft/TypeScript#33400 + const programAny = program as any; + const sysAny = sys as any; + const stats = { + typeCount: programAny.getTypeCount && programAny.getTypeCount(), + memoryUsage: sysAny.getMemoryUsage && sysAny.getMemoryUsage(), + relationCacheSizes: programAny.getRelationCacheSizes && programAny.getRelationCacheSizes(), + }; + + return {diagnostics , stats}; }; diff --git a/source/lib/index.ts b/source/lib/index.ts index 9bd6bc26..e7b47491 100644 --- a/source/lib/index.ts +++ b/source/lib/index.ts @@ -2,14 +2,11 @@ import * as path from 'path'; import * as readPkgUp from 'read-pkg-up'; import * as pathExists from 'path-exists'; import globby from 'globby'; -import {getDiagnostics as getTSDiagnostics} from './compiler'; +import {getTypeScriptResults as getTSDiagnostics} from './compiler'; import loadConfig from './config'; import getCustomDiagnostics from './rules'; -import {Context, Config} from './interfaces'; - -interface Options { - cwd: string; -} +import {Context, Config, Options} from './interfaces'; +import {verifyMemorySize} from './memory-validator'; const findTypingsFile = async (pkg: any, options: Options) => { const typings = pkg.types || pkg.typings || 'index.d.ts'; @@ -43,12 +40,21 @@ const findTestFiles = async (typingsFile: string, options: Options & {config: Co return testFiles; }; +// The default options if you were to run tsd on its own +const defaultOptions: Options = { + cwd: process.cwd(), + verify: false, + writeSnapshot: false, + sizeDelta: 10 +}; + /** * Type check the type definition of the project. * - * @returns A promise which resolves the diagnostics of the type definition. + * @returns A promise which resolves the run results with diagnostics and stats for the type definitions. */ -export default async (options: Options = {cwd: process.cwd()}) => { +export default async (inputOptions: Partial = defaultOptions) => { + const options = {...defaultOptions, ...inputOptions}; const {pkg} = await readPkgUp({cwd: options.cwd}); if (!pkg) { @@ -66,15 +72,18 @@ export default async (options: Options = {cwd: process.cwd()}) => { }); const context: Context = { - cwd: options.cwd, + options, pkg, typingsFile, testFiles, - config + config, }; + const tsResults = getTSDiagnostics(context); + return [ ...getCustomDiagnostics(context), - ...getTSDiagnostics(context) + ...tsResults.diagnostics, + ...await verifyMemorySize(context, tsResults.stats) ]; }; diff --git a/source/lib/interfaces.ts b/source/lib/interfaces.ts index 0dd85430..46b4093d 100644 --- a/source/lib/interfaces.ts +++ b/source/lib/interfaces.ts @@ -12,11 +12,18 @@ export interface Config { export type RawConfig = Partial>; export interface Context { - cwd: string; pkg: any; typingsFile: string; testFiles: string[]; config: Config; + options: Options +} + +export interface Options { + cwd: string; + verify: boolean; + writeSnapshot: boolean; + sizeDelta: number; } export enum DiagnosticCode { @@ -38,6 +45,17 @@ export interface Diagnostic { column?: number; } +export interface ProjectSizeStats { + typeCount: number; + memoryUsage: number; + relationCacheSizes: object; +} + +export interface RunResults { + diagnostics: Diagnostic[]; + stats: ProjectSizeStats; +} + export interface Location { fileName: string; start: number; diff --git a/source/lib/memory-validator.ts b/source/lib/memory-validator.ts new file mode 100644 index 00000000..e28f71cc --- /dev/null +++ b/source/lib/memory-validator.ts @@ -0,0 +1,85 @@ +import * as path from 'path'; +import {ProjectSizeStats, Diagnostic, Context} from './interfaces'; +import * as pathExists from 'path-exists'; +import {writeFileSync, readFileSync} from 'fs'; + +const statsFilename = '.tsd-memory-check-results.json'; + +const findStatsFile = async (context: Context) => { + const rootFileExists = await pathExists(path.join(context.options.cwd, statsFilename)); + if (rootFileExists) { + return path.join(context.options.cwd, statsFilename); + } + + const subfolderFileExists = await pathExists(path.join(context.options.cwd, '.tsd', statsFilename)); + if (subfolderFileExists) { + return path.join(context.options.cwd, '.tsd', statsFilename); + } + + return; +}; + +/** + * Get a list of TypeScript diagnostics, and memory size within the current context. + * + * @param context - The context object. + * @param runStats - An object which has the memory stats from a run. + * @returns An array of diagnostics + */ +export const verifyMemorySize = async (context: Context, runStats: ProjectSizeStats): Promise => { + const existingStatsPath = await findStatsFile(context); + + const writeJSON = (message: string) => { + const rootFilePath = path.join(context.options.cwd, statsFilename); + writeFileSync(rootFilePath, JSON.stringify(runStats, null, ' '), 'utf8'); + return [{ + fileName: rootFilePath, + message, + severity: 'warning' as 'warning' + }]; + }; + + if (!existingStatsPath) { + return writeJSON('Recording the stats for memory size in your types'); + } + + if (context.options.writeSnapshot) { + return writeJSON('Updated the stats for memory size in your types'); + } + + const existingResults = JSON.parse(readFileSync(existingStatsPath, 'utf8')) as ProjectSizeStats; + const validate = (prev: number, current: number) => { + const largerPrev = (prev / 100) * (context.options.sizeDelta + 100); + return current < largerPrev; + }; + + const names: string[] = []; + + // This is an approximation, and would likely change between versions, so the number is + // conservative + const typescriptTypes = 8500; + + if (!validate(existingResults.typeCount - typescriptTypes, runStats.typeCount - typescriptTypes)) { + names.push(`- Type Count raised by ${runStats.typeCount - existingResults.typeCount}`); + } + + if (!validate(existingResults.memoryUsage, runStats.memoryUsage)) { + names.push(`- Memory usage raised by ${runStats.typeCount / existingResults.typeCount * 100}%`); + } + + if (names.length) { + const messages = [ + 'Failed due to memory changes for types being raised. Higher numbers', + 'can slow down the editor experience for consumers.', + 'If you\'d like to update the fixtures to keep these changes re-run tsd with --write', + '', ...names]; + + return [{ + fileName: existingStatsPath, + message: messages.join('\n '), + severity: 'error' as 'error' + }]; + } + + return []; +}; diff --git a/source/lib/rules/files-property.ts b/source/lib/rules/files-property.ts index c0e9f9fd..bfaac101 100644 --- a/source/lib/rules/files-property.ts +++ b/source/lib/rules/files-property.ts @@ -23,7 +23,7 @@ export default (context: Context): Diagnostic[] => { return []; } - const content = fs.readFileSync(path.join(context.cwd, 'package.json'), 'utf8'); + const content = fs.readFileSync(path.join(context.options.cwd, 'package.json'), 'utf8'); return [ { diff --git a/source/lib/rules/types-property.ts b/source/lib/rules/types-property.ts index 877b1914..2cff491c 100644 --- a/source/lib/rules/types-property.ts +++ b/source/lib/rules/types-property.ts @@ -13,7 +13,7 @@ export default (context: Context): Diagnostic[] => { const {pkg} = context; if (!pkg.types && pkg.typings) { - const content = fs.readFileSync(path.join(context.cwd, 'package.json'), 'utf8'); + const content = fs.readFileSync(path.join(context.options.cwd, 'package.json'), 'utf8'); return [ {