diff --git a/baselines/packages/mimir/test/no-duplicate-spread-property/default/test.ts.lint b/baselines/packages/mimir/test/no-duplicate-spread-property/default/test.ts.lint new file mode 100644 index 000000000..25ad11002 --- /dev/null +++ b/baselines/packages/mimir/test/no-duplicate-spread-property/default/test.ts.lint @@ -0,0 +1,119 @@ +export {}; + +declare function get(): T; + +declare class WithMethods { + foo(): void; + bar: () => void; + baz: string; +} + +const foo = 'foo'; + +({ + x: 1, + ~~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...{x: 2, y: 2}, + ~~~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + y: 1, + ...{x: 3}, +}); + +({ + foo, + ~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...{foo}, +}); + +({ + [foo]: 1, + ~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...{[foo]: 2}, +}); + +({ + '__@iterator': 1, + [Symbol.iterator]: 1, + ~~~~~~~~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...{[Symbol.iterator]: 2}, +}); + +({ + [get()]: 1, + ...{[get()]: 2}, +}); + +({ + [get<'foo'>()]: 1, + ...{[get<'foo'>()]: 2}, + ~~~~~~~~~~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...{[foo]: 3}, +}); + +({ + foo: 1, + bar: 1, + ~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + baz: 1, + ~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...get<{foo?: string, bar: number, baz: boolean | undefined}>(), +}); + +({ + foo: 1, + bar: 1, + ~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + baz: 1, + bas: 1, + ...get<{foo: string, bar: number, bas: number} | {bar: number, baz: boolean, bas?: number}>(), + ...Boolean() && {foo}, +}); + +{ + let a, b; + ({[foo]: a, foo: b, ...{}} = get<{foo: string}>()); +} + +({ + foo: 1, + bar: 1, + ~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + baz: 1, + ~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...get(), +}); + +({ + foo() {}, + bar: () => {}, + ~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + baz: get<() => void>(), + ~~~~~~~~~~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...get(), +}); + +({ + foo() {}, + ~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + bar: () => {}, + ~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + baz: get<() => void>(), + ~~~~~~~~~~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...get<{foo(): void, bar: () => void, baz: number}>(), +}); + +({ + ...get(), + ~~~~~~~~~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + foo() {}, + bar: () => {}, + baz: get<() => void>(), +}); + +({ + ...get<{foo: number, bar: number, baz: number}>(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + foo() {}, + bar: () => {}, + baz: get<() => void>(), +}); diff --git a/baselines/packages/mimir/test/no-duplicate-spread-property/loose/test.ts.lint b/baselines/packages/mimir/test/no-duplicate-spread-property/loose/test.ts.lint new file mode 100644 index 000000000..d719ed80a --- /dev/null +++ b/baselines/packages/mimir/test/no-duplicate-spread-property/loose/test.ts.lint @@ -0,0 +1,101 @@ +export {}; + +declare function get(): T; + +declare class WithMethods { + foo(): void; + bar: () => void; + baz: string; +} + +const foo = 'foo'; + +({ + x: 1, + ...{x: 2, y: 2}, + y: 1, + ...{x: 3}, +}); + +({ + foo, + ...{foo}, +}); + +({ + [foo]: 1, + ...{[foo]: 2}, +}); + +({ + '__@iterator': 1, + [Symbol.iterator]: 1, + ...{[Symbol.iterator]: 2}, +}); + +({ + [get()]: 1, + ...{[get()]: 2}, +}); + +({ + [get<'foo'>()]: 1, + ...{[get<'foo'>()]: 2}, + ...{[foo]: 3}, +}); + +({ + foo: 1, + bar: 1, + baz: 1, + ...get<{foo?: string, bar: number, baz: boolean | undefined}>(), +}); + +({ + foo: 1, + bar: 1, + baz: 1, + bas: 1, + ...get<{foo: string, bar: number, bas: number} | {bar: number, baz: boolean, bas?: number}>(), + ...Boolean() && {foo}, +}); + +{ + let a, b; + ({[foo]: a, foo: b, ...{}} = get<{foo: string}>()); +} + +({ + foo: 1, + bar: 1, + baz: 1, + ...get(), +}); + +({ + foo() {}, + bar: () => {}, + baz: get<() => void>(), + ...get(), +}); + +({ + foo() {}, + bar: () => {}, + baz: get<() => void>(), + ...get<{foo(): void, bar: () => void, baz: number}>(), +}); + +({ + ...get(), + foo() {}, + bar: () => {}, + baz: get<() => void>(), +}); + +({ + ...get<{foo: number, bar: number, baz: number}>(), + foo() {}, + bar: () => {}, + baz: get<() => void>(), +}); diff --git a/baselines/packages/mimir/test/no-duplicate-spread-property/pre260/test.ts.lint b/baselines/packages/mimir/test/no-duplicate-spread-property/pre260/test.ts.lint new file mode 100644 index 000000000..d719ed80a --- /dev/null +++ b/baselines/packages/mimir/test/no-duplicate-spread-property/pre260/test.ts.lint @@ -0,0 +1,101 @@ +export {}; + +declare function get(): T; + +declare class WithMethods { + foo(): void; + bar: () => void; + baz: string; +} + +const foo = 'foo'; + +({ + x: 1, + ...{x: 2, y: 2}, + y: 1, + ...{x: 3}, +}); + +({ + foo, + ...{foo}, +}); + +({ + [foo]: 1, + ...{[foo]: 2}, +}); + +({ + '__@iterator': 1, + [Symbol.iterator]: 1, + ...{[Symbol.iterator]: 2}, +}); + +({ + [get()]: 1, + ...{[get()]: 2}, +}); + +({ + [get<'foo'>()]: 1, + ...{[get<'foo'>()]: 2}, + ...{[foo]: 3}, +}); + +({ + foo: 1, + bar: 1, + baz: 1, + ...get<{foo?: string, bar: number, baz: boolean | undefined}>(), +}); + +({ + foo: 1, + bar: 1, + baz: 1, + bas: 1, + ...get<{foo: string, bar: number, bas: number} | {bar: number, baz: boolean, bas?: number}>(), + ...Boolean() && {foo}, +}); + +{ + let a, b; + ({[foo]: a, foo: b, ...{}} = get<{foo: string}>()); +} + +({ + foo: 1, + bar: 1, + baz: 1, + ...get(), +}); + +({ + foo() {}, + bar: () => {}, + baz: get<() => void>(), + ...get(), +}); + +({ + foo() {}, + bar: () => {}, + baz: get<() => void>(), + ...get<{foo(): void, bar: () => void, baz: number}>(), +}); + +({ + ...get(), + foo() {}, + bar: () => {}, + baz: get<() => void>(), +}); + +({ + ...get<{foo: number, bar: number, baz: number}>(), + foo() {}, + bar: () => {}, + baz: get<() => void>(), +}); diff --git a/baselines/packages/mimir/test/no-duplicate-spread-property/ts260/test.ts.lint b/baselines/packages/mimir/test/no-duplicate-spread-property/ts260/test.ts.lint new file mode 100644 index 000000000..d95094a7c --- /dev/null +++ b/baselines/packages/mimir/test/no-duplicate-spread-property/ts260/test.ts.lint @@ -0,0 +1,117 @@ +export {}; + +declare function get(): T; + +declare class WithMethods { + foo(): void; + bar: () => void; + baz: string; +} + +const foo = 'foo'; + +({ + x: 1, + ~~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...{x: 2, y: 2}, + ~~~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + y: 1, + ...{x: 3}, +}); + +({ + foo, + ~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...{foo}, +}); + +({ + [foo]: 1, + ...{[foo]: 2}, +}); + +({ + '__@iterator': 1, + [Symbol.iterator]: 1, + ...{[Symbol.iterator]: 2}, +}); + +({ + [get()]: 1, + ...{[get()]: 2}, +}); + +({ + [get<'foo'>()]: 1, + ...{[get<'foo'>()]: 2}, + ~~~~~~~~~~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...{[foo]: 3}, +}); + +({ + foo: 1, + bar: 1, + ~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + baz: 1, + ~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...get<{foo?: string, bar: number, baz: boolean | undefined}>(), +}); + +({ + foo: 1, + bar: 1, + ~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + baz: 1, + bas: 1, + ...get<{foo: string, bar: number, bas: number} | {bar: number, baz: boolean, bas?: number}>(), + ...Boolean() && {foo}, +}); + +{ + let a, b; + ({[foo]: a, foo: b, ...{}} = get<{foo: string}>()); +} + +({ + foo: 1, + bar: 1, + ~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + baz: 1, + ~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...get(), +}); + +({ + foo() {}, + bar: () => {}, + ~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + baz: get<() => void>(), + ~~~~~~~~~~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...get(), +}); + +({ + foo() {}, + ~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + bar: () => {}, + ~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + baz: get<() => void>(), + ~~~~~~~~~~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + ...get<{foo(): void, bar: () => void, baz: number}>(), +}); + +({ + ...get(), + ~~~~~~~~~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + foo() {}, + bar: () => {}, + baz: get<() => void>(), +}); + +({ + ...get<{foo: number, bar: number, baz: number}>(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [error no-duplicate-spread-property: Property is overridden later.] + foo() {}, + bar: () => {}, + baz: get<() => void>(), +}); diff --git a/packages/mimir/README.md b/packages/mimir/README.md index a9a4eb7a8..a283ce35c 100644 --- a/packages/mimir/README.md +++ b/packages/mimir/README.md @@ -27,6 +27,7 @@ Rule | Description | Difference to TSLint rule / Why you should use it `no-case-declaration` | Disallow `let`, `class` and `enum` in case blocks. These are visible within the whole switch statement body but not defined in other case clauses. The compiler currently doesn't warn about such uses. You should use a block to restrict the scope of the declarations. | TSLint has no similar rule, ESLint has `no-case-declarations` which forbids function declarations as well. `no-debugger` | Ban `debugger;` statements from your production code. | Performance! `no-duplicate-case` | Detects `switch` statements where multiple `case` clauses check for the same value. *uses type information if available* | This implementation tries to infer the value instead of just comparing the source code. +`no-duplicate-spread-property` | Detects properties in object literals that are overridden by a spreaded object. | TSLint has no such rule. `no-fallthrough` | Prevents unintentional fallthough in `switch` statements from one case to another. If the fallthrough is intended, add a comment that matches `/^\s*falls? ?through\b/i`. | Allows more comment variants such as `fallthrough` or `fall through`. `no-inferred-empty-object` | Warns if a type parameter is inferred as `{}` because the compiler cannot find any inference site. *requires type information* | Really checks every type parameter of function, method and constructor calls. Correctly handles type parameters from JSDoc comments. Recognises type parameter defaults on all merged declarations. `no-invalid-assertion` | Disallows asserting a literal type to a different literal type of the same widened type, e.g. `'foo' as 'bar'`. *requires type information* | TSLint has no similar rule. diff --git a/packages/mimir/recommended.yaml b/packages/mimir/recommended.yaml index b5d61061c..c3e0b3702 100644 --- a/packages/mimir/recommended.yaml +++ b/packages/mimir/recommended.yaml @@ -7,6 +7,7 @@ rules: no-case-declaration: error no-debugger: error no-duplicate-case: error + no-duplicate-spread-property: error no-fallthrough: error no-inferred-empty-object: error no-invalid-assertion: error diff --git a/packages/mimir/src/rules/no-duplicate-spread-property.ts b/packages/mimir/src/rules/no-duplicate-spread-property.ts new file mode 100644 index 000000000..de4533080 --- /dev/null +++ b/packages/mimir/src/rules/no-duplicate-spread-property.ts @@ -0,0 +1,110 @@ +import { TypedRule, excludeDeclarationFiles, RuleSupportsContext } from '@fimbul/ymir'; +import * as ts from 'typescript'; +import { isReassignmentTarget, isObjectType, unionTypeParts, isClassLikeDeclaration } from 'tsutils'; +import { isStrictNullChecksEnabled } from '../utils'; + +interface PropertyInfo { + known: boolean; + names: ts.__String[]; + assignedNames: ts.__String[]; +} + +const emptyPropertyInfo: PropertyInfo = { + known: false, + names: [], + assignedNames: [], +}; + +@excludeDeclarationFiles +export class Rule extends TypedRule { + public static supports(_sourceFile: ts.SourceFile, context: RuleSupportsContext) { + return !ts.version.startsWith('2.4.') && isStrictNullChecksEnabled(context.program!.getCompilerOptions()); + } + + public apply() { + const checkedObjects = new Set(); + for (const node of this.context.getFlatAst()) { + if ( + node.kind === ts.SyntaxKind.SpreadAssignment && + !checkedObjects.has(node.parent!.pos) && + !isReassignmentTarget(node.parent) + ) { + checkedObjects.add(node.parent!.pos); + this.checkObject(node.parent); + } + } + } + + private checkObject({properties}: ts.ObjectLiteralExpression) { + const propertiesSeen = new Set(); + for (let i = properties.length - 1; i >= 0; --i) { + const info = this.getPropertyInfo(properties[i]); + if (info.known && info.names.every((name) => propertiesSeen.has(name))) + this.addFailureAtNode(properties[i], 'Property is overridden later.'); + for (const name of info.assignedNames) + propertiesSeen.add(name); + } + } + + private getPropertyInfo(property: ts.ObjectLiteralElementLike): PropertyInfo { + switch (property.kind) { + case ts.SyntaxKind.SpreadAssignment: + return this.getPropertyInfoFromSpread(property.expression); + case ts.SyntaxKind.ShorthandPropertyAssignment: + return { + known: true, + names: [property.name.escapedText], + assignedNames: [property.name.escapedText], + }; + default: { + const symbol = this.checker.getSymbolAtLocation(property.name); + if (symbol === undefined) + return emptyPropertyInfo; + return { + known: true, + names: [symbol.escapedName], + assignedNames: [symbol.escapedName], + }; + } + } + } + + private getPropertyInfoFromSpread(node: ts.Expression): PropertyInfo { + const type = this.checker.getTypeAtLocation(node); + return unionTypeParts(type).map(getPropertyInfoFromType).reduce(combinePropertyInfo); + } + +} +function getPropertyInfoFromType(type: ts.Type): PropertyInfo { + if (!isObjectType(type)) + return emptyPropertyInfo; + const result: PropertyInfo = { + known: (type.flags & ts.TypeFlags.Any) !== 0 || + type.getStringIndexType() === undefined && type.getNumberIndexType() === undefined, + names: [], + assignedNames: [], + }; + for (const prop of type.getProperties()) { + if (isClassMethod(prop)) + continue; + if ((prop.flags & ts.SymbolFlags.Optional) === 0) + result.assignedNames.push(prop.escapedName); + result.names.push(prop.escapedName); + } + return result; +} +function isClassMethod(prop: ts.Symbol): boolean | undefined { + if (prop.flags & ts.SymbolFlags.Method && prop.declarations !== undefined) + for (const declaration of prop.declarations) + if (isClassLikeDeclaration(declaration.parent!)) + return true; + return false; +} + +function combinePropertyInfo(a: PropertyInfo, b: PropertyInfo): PropertyInfo { + return { + known: a.known && b.known, + names: [...a.names, ...b.names], + assignedNames: a.assignedNames.filter((name) => b.assignedNames.includes(name)), + }; +} diff --git a/packages/mimir/test/no-duplicate-spread-property/.wotanrc.yaml b/packages/mimir/test/no-duplicate-spread-property/.wotanrc.yaml new file mode 100644 index 000000000..c76b77fcf --- /dev/null +++ b/packages/mimir/test/no-duplicate-spread-property/.wotanrc.yaml @@ -0,0 +1,2 @@ +rules: + no-duplicate-spread-property: error diff --git a/packages/mimir/test/no-duplicate-spread-property/default.test.json b/packages/mimir/test/no-duplicate-spread-property/default.test.json new file mode 100644 index 000000000..92b4d9e1c --- /dev/null +++ b/packages/mimir/test/no-duplicate-spread-property/default.test.json @@ -0,0 +1,3 @@ +{ + "typescriptVersion": ">= 2.7.0" +} diff --git a/packages/mimir/test/no-duplicate-spread-property/loose.test.json b/packages/mimir/test/no-duplicate-spread-property/loose.test.json new file mode 100644 index 000000000..44860f0aa --- /dev/null +++ b/packages/mimir/test/no-duplicate-spread-property/loose.test.json @@ -0,0 +1,3 @@ +{ + "project": "tsconfig-loose.json" +} diff --git a/packages/mimir/test/no-duplicate-spread-property/pre260.test.json b/packages/mimir/test/no-duplicate-spread-property/pre260.test.json new file mode 100644 index 000000000..44754ce9a --- /dev/null +++ b/packages/mimir/test/no-duplicate-spread-property/pre260.test.json @@ -0,0 +1,3 @@ +{ + "typescriptVersion": "<2.6.0" +} diff --git a/packages/mimir/test/no-duplicate-spread-property/test.ts b/packages/mimir/test/no-duplicate-spread-property/test.ts new file mode 100644 index 000000000..d719ed80a --- /dev/null +++ b/packages/mimir/test/no-duplicate-spread-property/test.ts @@ -0,0 +1,101 @@ +export {}; + +declare function get(): T; + +declare class WithMethods { + foo(): void; + bar: () => void; + baz: string; +} + +const foo = 'foo'; + +({ + x: 1, + ...{x: 2, y: 2}, + y: 1, + ...{x: 3}, +}); + +({ + foo, + ...{foo}, +}); + +({ + [foo]: 1, + ...{[foo]: 2}, +}); + +({ + '__@iterator': 1, + [Symbol.iterator]: 1, + ...{[Symbol.iterator]: 2}, +}); + +({ + [get()]: 1, + ...{[get()]: 2}, +}); + +({ + [get<'foo'>()]: 1, + ...{[get<'foo'>()]: 2}, + ...{[foo]: 3}, +}); + +({ + foo: 1, + bar: 1, + baz: 1, + ...get<{foo?: string, bar: number, baz: boolean | undefined}>(), +}); + +({ + foo: 1, + bar: 1, + baz: 1, + bas: 1, + ...get<{foo: string, bar: number, bas: number} | {bar: number, baz: boolean, bas?: number}>(), + ...Boolean() && {foo}, +}); + +{ + let a, b; + ({[foo]: a, foo: b, ...{}} = get<{foo: string}>()); +} + +({ + foo: 1, + bar: 1, + baz: 1, + ...get(), +}); + +({ + foo() {}, + bar: () => {}, + baz: get<() => void>(), + ...get(), +}); + +({ + foo() {}, + bar: () => {}, + baz: get<() => void>(), + ...get<{foo(): void, bar: () => void, baz: number}>(), +}); + +({ + ...get(), + foo() {}, + bar: () => {}, + baz: get<() => void>(), +}); + +({ + ...get<{foo: number, bar: number, baz: number}>(), + foo() {}, + bar: () => {}, + baz: get<() => void>(), +}); diff --git a/packages/mimir/test/no-duplicate-spread-property/ts260.test.json b/packages/mimir/test/no-duplicate-spread-property/ts260.test.json new file mode 100644 index 000000000..3ab0610ff --- /dev/null +++ b/packages/mimir/test/no-duplicate-spread-property/ts260.test.json @@ -0,0 +1,3 @@ +{ + "typescriptVersion": "~2.6.0" +} diff --git a/packages/mimir/test/no-duplicate-spread-property/tsconfig-loose.json b/packages/mimir/test/no-duplicate-spread-property/tsconfig-loose.json new file mode 100644 index 000000000..04608d4ca --- /dev/null +++ b/packages/mimir/test/no-duplicate-spread-property/tsconfig-loose.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "strict": false, + "target": "esnext" + } +} diff --git a/packages/mimir/test/no-duplicate-spread-property/tsconfig.json b/packages/mimir/test/no-duplicate-spread-property/tsconfig.json new file mode 100644 index 000000000..3338b50e9 --- /dev/null +++ b/packages/mimir/test/no-duplicate-spread-property/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "strict": true, + "target": "esnext" + } +}