diff --git a/README.md b/README.md index 87bc646..b1c5fa0 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,10 @@ name | type | description `--is` | number? | fail if coverage rate !== this value(Added in `v2.6`) `--update` | boolean? | update "typeCoverage" in package.json to current result(Added in `v2.6`) `--ignore-unread` | boolean? | allow writes to variables with implicit any types(Added in `v2.14`) +`--ignore-nested` | boolean? | ignore any in type arguments, eg: `Promise`(Added in `v2.16`) +`--ignore-as-assertion` | boolean? | ignore as assertion, eg: `foo as string`(Added in `v2.16`) +`--ignore-type-assertion` | boolean? | ignore type assertion, eg: `foo`(Added in `v2.16`) +`--ignore-non-null-assertion` | boolean? | ignore non-null assertion, eg: `foo!`(Added in `v2.16`) ### strict mode @@ -95,7 +99,11 @@ This tool will ignore the files, eg: `--ignore-files "demo1/*.ts" --ignore-files "strict": true, // same as --strict (Added in `v2.11`) "suppressError": true, // same as --suppressError (Added in `v2.11`) "update": true, // same as --update (Added in `v2.11`) - "ignoreUnread": true // same as --ignore-unread (Added in `v2.14`) + "ignoreUnread": true, // same as --ignore-unread (Added in `v2.14`) + "ignoreNested": true, // same as --ignore-nested (Added in `v2.16`) + "ignoreAsAssertion": true, // same as --ignore-as-assertion (Added in `v2.16`) + "ignoreTypeAssertion": true, // same as --ignore-type-assertion (Added in `v2.16`) + "ignoreNonNullAssertion": true // same as --ignore-non-null-assertion (Added in `v2.16`) }, ``` @@ -154,6 +162,10 @@ export interface LintOptions { absolutePath?: boolean, // Added in v2.4 processAny?: ProccessAny, // Added in v2.7 ignoreUnreadAnys: boolean, // Added in v2.14 + ignoreNested: boolean // Added in v2.16 + ignoreAsAssertion: boolean // Added in v2.16 + ignoreTypeAssertion: boolean // Added in v2.16 + ignoreNonNullAssertion: boolean // Added in v2.16 } export interface FileTypeCheckResult { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index cac5a88..36181cb 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -18,18 +18,22 @@ function showToolVersion() { function printHelp() { console.log(`type-coverage [options] --p, --project string? tell the CLI where is the tsconfig.json ---detail boolean? show detail ---at-least number? fail if coverage rate < this value ---debug boolean? show debug info ---strict boolean? strict mode ---ignore-catch boolean? ignore catch ---cache boolean? enable cache ---ignore-files string[]? ignore files ---ignore-unread boolean? allow writes to variables with implicit any types --h,--help boolean? show help ---is number? fail if coverage rate !== this value ---update boolean? update "typeCoverage" in package.json to current result +-p, --project string? tell the CLI where is the tsconfig.json +--detail boolean? show detail +--at-least number? fail if coverage rate < this value +--debug boolean? show debug info +--strict boolean? strict mode +--ignore-catch boolean? ignore catch +--cache boolean? enable cache +--ignore-files string[]? ignore files +--ignore-unread boolean? allow writes to variables with implicit any types +-h,--help boolean? show help +--is number? fail if coverage rate !== this value +--update boolean? update "typeCoverage" in package.json to current result +--ignore-nested boolean? ignore any in type arguments, eg: Promise +--ignore-as-assertion boolean? ignore as assertion, eg: foo as string +--ignore-type-assertion boolean? ignore type assertion, eg: foo +--ignore-non-null-assertion boolean? ignore non-null assertion, eg: foo! `) } @@ -54,6 +58,11 @@ interface CliArgs extends BaseArgs { ['ignore-files']?: string | string[] ['at-least']: number ['ignore-unread']: boolean + + ['ignore-nested']: boolean + ['ignore-as-assertion']: boolean + ['ignore-type-assertion']: boolean + ['ignore-non-null-assertion']: boolean } interface PkgArgs extends BaseArgs { @@ -61,6 +70,11 @@ interface PkgArgs extends BaseArgs { ignoreFiles?: string | string[] ignoreUnread: boolean atLeast: boolean + + ignoreNested: boolean + ignoreAsAssertion: boolean + ignoreTypeAssertion: boolean + ignoreNonNullAssertion: boolean } interface PackageJson { @@ -83,15 +97,35 @@ async function executeCommandLine() { process.exit(0) } - const { atLeast, debug, detail, enableCache, ignoreCatch, ignoreFiles, ignoreUnread, is, project, strict, update } = await getTarget(argv); + const { + atLeast, + debug, + detail, + enableCache, + ignoreCatch, + ignoreFiles, + ignoreUnread, + is, + project, + strict, + update, + ignoreNested, + ignoreAsAssertion, + ignoreTypeAssertion, + ignoreNonNullAssertion + } = await getTarget(argv); const { correctCount, totalCount, anys } = await lint(project, { - debug: debug, - strict: strict, - enableCache: enableCache, - ignoreCatch: ignoreCatch, - ignoreFiles: ignoreFiles, + debug, + strict, + enableCache, + ignoreCatch, + ignoreFiles, ignoreUnreadAnys: ignoreUnread, + ignoreNested, + ignoreAsAssertion: ignoreAsAssertion, + ignoreTypeAssertion, + ignoreNonNullAssertion, }); const percent = Math.floor(10000 * correctCount / totalCount) / 100 @@ -159,8 +193,28 @@ async function getTarget(argv: CliArgs) { const project = getArgOrCfgVal(['p', 'project']) || '.' const strict = getArgOrCfgVal(['strict']) const update = getArgOrCfgVal(['update']) - - return { atLeast, debug, detail, enableCache, ignoreCatch, ignoreFiles, ignoreUnread, is, project, strict, update }; + const ignoreNested = getArgOrCfgVal(['ignore-nested', 'ignoreNested']) + const ignoreAsAssertion = getArgOrCfgVal(['ignore-as-assertion', 'ignoreAsAssertion']) + const ignoreTypeAssertion = getArgOrCfgVal(['ignore-type-assertion', 'ignoreTypeAssertion']) + const ignoreNonNullAssertion = getArgOrCfgVal(['ignore-non-null-assertion', 'ignoreNonNullAssertion']) + + return { + atLeast, + debug, + detail, + enableCache, + ignoreCatch, + ignoreFiles, + ignoreUnread, + is, + project, + strict, + update, + ignoreNested, + ignoreAsAssertion, + ignoreTypeAssertion, + ignoreNonNullAssertion, + }; } async function saveTarget(target: number) { diff --git a/packages/core/src/checker.ts b/packages/core/src/checker.ts index d7bce5c..ab6c99f 100644 --- a/packages/core/src/checker.ts +++ b/packages/core/src/checker.ts @@ -46,8 +46,8 @@ function collectData(node: ts.Node, context: FileContext) { if (types.length > 0) { context.typeCheckResult.totalCount++ - if (types.every((t) => typeIsStrictAny(t, context.strict))) { - const kind = types.every((t) => typeIsStrictAny(t, false)) ? FileAnyInfoKind.any : FileAnyInfoKind.containsAny + if (types.every((t) => typeIsAnyOrInTypeArguments(t, context.strict && !context.ignoreNested))) { + const kind = types.every((t) => typeIsAnyOrInTypeArguments(t, false)) ? FileAnyInfoKind.any : FileAnyInfoKind.containsAny const success = collectAny(node, context, kind) if (!success) { collectNotAny(node, context, type) @@ -58,14 +58,14 @@ function collectData(node: ts.Node, context: FileContext) { } } -function typeIsStrictAny(type: ts.Type, strict: boolean): boolean { +function typeIsAnyOrInTypeArguments(type: ts.Type, anyCanBeInTypeArguments: boolean): boolean { if (type.flags === ts.TypeFlags.Any) { return (type as unknown as { intrinsicName: string }).intrinsicName === 'any' } - if (strict && type.flags === ts.TypeFlags.Object) { + if (anyCanBeInTypeArguments && type.flags === ts.TypeFlags.Object) { const typeArguments = (type as ts.TypeReference).typeArguments if (typeArguments) { - return typeArguments.some((typeArgument) => typeIsStrictAny(typeArgument, strict)) + return typeArguments.some((typeArgument) => typeIsAnyOrInTypeArguments(typeArgument, anyCanBeInTypeArguments)) } } return false @@ -95,10 +95,22 @@ function checkNodes(nodes: ts.NodeArray | undefined, context: FileConte } } +const isTypeAssertionExpression = ts.isTypeAssertionExpression || ts.isTypeAssertion + function checkTypeAssertion(node: ts.Node, context: FileContext, kind: FileAnyInfoKind) { if (context.strict) { + if (kind === FileAnyInfoKind.unsafeNonNull && context.ignoreNonNullAssertion) { + return + } + if (kind === FileAnyInfoKind.unsafeAs && context.ignoreAsAssertion) { + return + } + if (kind === FileAnyInfoKind.unsafeTypeAssertion && context.ignoreTypeAssertion) { + return + } + // include `foo as any` and `foo` - if ((ts.isAsExpression(node) || ts.isTypeAssertion(node)) && node.type.kind !== ts.SyntaxKind.AnyKeyword) { + if ((ts.isAsExpression(node) || isTypeAssertionExpression(node)) && node.type.kind !== ts.SyntaxKind.AnyKeyword) { // exclude `foo as const` and `foo` if (ts.isTypeReferenceNode(node.type) && node.type.getText() === 'const') { return @@ -334,7 +346,7 @@ export function checkNode(node: ts.Node | undefined, context: FileContext): void checkNodes(node.typeArguments, context) return } - if (ts.isTypeAssertion(node)) { + if (isTypeAssertionExpression(node)) { checkTypeAssertion(node, context, FileAnyInfoKind.unsafeTypeAssertion) checkNode(node.expression, context) checkNode(node.type, context) diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 3c6739d..492b22e 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -105,6 +105,10 @@ export async function lint(project: string, options?: Partial) { processAny: lintOptions.processAny, checker, ingoreMap, + ignoreNested: lintOptions.ignoreNested, + ignoreAsAssertion: lintOptions.ignoreAsAssertion, + ignoreTypeAssertion: lintOptions.ignoreTypeAssertion, + ignoreNonNullAssertion: lintOptions.ignoreNonNullAssertion, } sourceFile.forEachChild(node => { @@ -155,6 +159,10 @@ const defaultLintOptions: LintOptions = { ignoreFiles: undefined, ignoreUnreadAnys: false, fileCounts: false, + ignoreNested: false, + ignoreAsAssertion: false, + ignoreTypeAssertion: false, + ignoreNonNullAssertion: false, } /** @@ -216,6 +224,10 @@ export function lintSync(compilerOptions: ts.CompilerOptions, rootNames: string[ processAny: lintOptions.processAny, checker, ingoreMap, + ignoreNested: lintOptions.ignoreNested, + ignoreAsAssertion: lintOptions.ignoreAsAssertion, + ignoreTypeAssertion: lintOptions.ignoreTypeAssertion, + ignoreNonNullAssertion: lintOptions.ignoreNonNullAssertion, } sourceFile.forEachChild(node => { diff --git a/packages/core/src/interfaces.ts b/packages/core/src/interfaces.ts index ad12f1a..afd1f1b 100644 --- a/packages/core/src/interfaces.ts +++ b/packages/core/src/interfaces.ts @@ -36,32 +36,46 @@ export const enum FileAnyInfoKind { */ export type ProccessAny = (node: ts.Node, context: FileContext) => boolean -export interface LintOptions { - debug: boolean, +export interface LintOptions extends CommonOptions { files?: string[], oldProgram?: ts.Program, - strict: boolean, enableCache: boolean, - ignoreCatch: boolean, ignoreFiles?: string | string[], - ignoreUnreadAnys: boolean, fileCounts: boolean, absolutePath?: boolean, +} + +interface CommonOptions { + debug: boolean, + strict: boolean, + ignoreCatch: boolean, + ignoreUnreadAnys: boolean, processAny?: ProccessAny, + /** + * Promise + */ + ignoreNested: boolean + /** + * foo as string + */ + ignoreAsAssertion: boolean + /** + * foo + */ + ignoreTypeAssertion: boolean + /** + * foo! + */ + ignoreNonNullAssertion: boolean } -export interface FileContext { +export interface FileContext extends CommonOptions { file: string sourceFile: ts.SourceFile typeCheckResult: FileTypeCheckResult - debug: boolean - strict: boolean checker: ts.TypeChecker - ignoreCatch: boolean - ignoreUnreadAnys: boolean catchVariables: { [variable: string]: boolean } ingoreMap: { [file: string]: Set } - processAny?: ProccessAny } interface TypeCheckCache extends FileTypeCheckResult {