From cdd9eef98bbc712c42282339b3a35cbff840ba09 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Sat, 15 Apr 2017 17:20:13 -0700 Subject: [PATCH] Add 'object-literal-contextual-type' rule --- src/configs/all.ts | 4 +- src/configs/latest.ts | 3 +- src/configs/recommended.ts | 1 + src/rules/completedDocsRule.ts | 3 +- src/rules/noUnusedVariableRule.ts | 8 +- src/rules/objectLiteralContextualTypeRule.ts | 91 +++++++++++++++++++ src/rules/spaceBeforeFunctionParenRule.ts | 1 + src/rules/trailingCommaRule.ts | 4 +- src/rules/typedefWhitespaceRule.ts | 2 +- src/rules/unifiedSignaturesRule.ts | 5 +- src/test.ts | 5 +- src/test/parse.ts | 6 +- test/configurationTests.ts | 2 +- test/formatters/jsonFormatterTests.ts | 4 +- .../test.ts.lint | 25 +++++ .../tslint.json | 8 ++ test/utilsTests.ts | 1 + tslint.json | 1 + 18 files changed, 154 insertions(+), 20 deletions(-) create mode 100644 src/rules/objectLiteralContextualTypeRule.ts create mode 100644 test/rules/object-literal-contextual-type/test.ts.lint create mode 100644 test/rules/object-literal-contextual-type/tslint.json diff --git a/src/configs/all.ts b/src/configs/all.ts index bef49f49f12..e0205bdc77b 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -20,8 +20,7 @@ import { join as joinPaths } from "path"; import { findRule } from "../ruleLoader"; import { hasOwnProperty } from "../utils"; -// tslint:disable object-literal-sort-keys -// tslint:disable object-literal-key-quotes +// tslint:disable object-literal-sort-keys object-literal-key-quotes object-literal-contextual-type export const rules = { // TypeScript Specific @@ -117,6 +116,7 @@ export const rules = { "no-use-before-declare": true, "no-var-keyword": true, "no-void-expression": true, + "object-literal-contextual-type": true, "radix": true, "restrict-plus-operands": true, "strict-boolean-expressions": true, diff --git a/src/configs/latest.ts b/src/configs/latest.ts index 406b9c662c8..abc27e5e90b 100644 --- a/src/configs/latest.ts +++ b/src/configs/latest.ts @@ -15,8 +15,7 @@ * limitations under the License. */ -// tslint:disable object-literal-sort-keys -// tslint:disable:object-literal-key-quotes +// tslint:disable object-literal-sort-keys object-literal-key-quotes object-literal-contextual-type export const rules = { // added in v5.1 "align": { diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index 9d79cb9ca28..844edab604c 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +// tslint:disable object-literal-contextual-type export const rules = { "adjacent-overload-signatures": true, "align": { diff --git a/src/rules/completedDocsRule.ts b/src/rules/completedDocsRule.ts index e1480fd47fd..5fac5749fc0 100644 --- a/src/rules/completedDocsRule.ts +++ b/src/rules/completedDocsRule.ts @@ -92,6 +92,7 @@ type BlockOrClassRequirement = BlockRequirement | ClassRequirement; export class Rule extends Lint.Rules.TypedRule { public static FAILURE_STRING_EXIST = "Documentation must exist for "; + // tslint:disable object-literal-contextual-type (https://github.com/palantir/tslint/issues/2428) public static defaultArguments = [ ARGUMENT_CLASSES, ARGUMENT_FUNCTIONS, @@ -217,7 +218,7 @@ export class Rule extends Lint.Rules.TypedRule { type: "style", typescriptOnly: false, }; - /* tslint:enable:object-literal-sort-keys */ + /* tslint:enable:object-literal-sort-keys object-literal-contextual-type */ public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { const options = this.getOptions(); diff --git a/src/rules/noUnusedVariableRule.ts b/src/rules/noUnusedVariableRule.ts index c2c3417d9d6..fc9e3612049 100644 --- a/src/rules/noUnusedVariableRule.ts +++ b/src/rules/noUnusedVariableRule.ts @@ -363,7 +363,13 @@ function getUnusedCheckedProgram(program: ts.Program, checkParameters: boolean): } function makeUnusedCheckedProgram(program: ts.Program, checkParameters: boolean): ts.Program { - const options = { ...program.getCompilerOptions(), noUnusedLocals: true, ...(checkParameters ? { noUnusedParameters: true } : null) }; + const options: ts.CompilerOptions = { + ...program.getCompilerOptions(), + noUnusedLocals: true, + }; + if (checkParameters) { + options.noUnusedParameters = true; + } const sourceFilesByName = new Map(program.getSourceFiles().map<[string, ts.SourceFile]>((s) => [s.fileName, s])); // tslint:disable object-literal-sort-keys return ts.createProgram(Array.from(sourceFilesByName.keys()), options, { diff --git a/src/rules/objectLiteralContextualTypeRule.ts b/src/rules/objectLiteralContextualTypeRule.ts new file mode 100644 index 00000000000..f77b6c3e269 --- /dev/null +++ b/src/rules/objectLiteralContextualTypeRule.ts @@ -0,0 +1,91 @@ +/** + * @license + * Copyright 2017 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isArrayLiteralExpression, isObjectLiteralExpression, isPropertyAssignment } from "tsutils"; +import * as ts from "typescript"; + +import * as Lint from "../index"; + +export class Rule extends Lint.Rules.TypedRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "object-literal-contextual-type", + description: "Requires that every object literal has a contextual type.", + rationale: Lint.Utils.dedent` + An object literal with no type does not have excess properties checked. + + For example: + + interface I { x: number; } + function f(): I { + const res = { x: 0, y: 0 }; + return res; + } + + This has no compile error, but an excess property \`y\`. + The excess property can be detected by writing a type annotatino \`const res: I = { x: 0, y: 0 };\`.`, + optionsDescription: "Not configurable.", + options: null, + optionExamples: [true], + type: "functionality", + typescriptOnly: true, + requiresTypeInfo: true, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = "Object literal has no contextual type."; + + public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { + return this.applyWithFunction(sourceFile, (ctx) => walk(ctx, program.getTypeChecker())); + } +} + +function walk(ctx: Lint.WalkContext, checker: ts.TypeChecker): void { + return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void { + if (isObjectLiteralExpression(node)) { + check(node); + } + return ts.forEachChild(node, cb); + }); + + function check(node: ts.ObjectLiteralExpression): void { + // Allow `{}`, because obviously it does not have excess properties. + if (node.properties.length === 0) { + return; + } + + // Allow an object literal inside another object literal or array literal (recursively) typed as 'any'. + // Normally the nested objects will not have a contextual type, so must traverse upwards to look for it. + let contextualNode: ts.Expression = node; + + do { + if (checker.getContextualType(contextualNode) !== undefined) { + return; + } + + const parent = contextualNode.parent!; + if (isPropertyAssignment(parent)) { + contextualNode = parent.parent! as ts.ObjectLiteralExpression; + } else if (isArrayLiteralExpression(parent)) { + contextualNode = parent; + } else { + ctx.addFailureAtNode(node, Rule.FAILURE_STRING); + return; + } + } while (true); + } +} diff --git a/src/rules/spaceBeforeFunctionParenRule.ts b/src/rules/spaceBeforeFunctionParenRule.ts index a4cd0c4763f..e357abcc17a 100644 --- a/src/rules/spaceBeforeFunctionParenRule.ts +++ b/src/rules/spaceBeforeFunctionParenRule.ts @@ -18,6 +18,7 @@ import * as ts from "typescript"; import * as Lint from "../index"; +// tslint:disable-next-line object-literal-contextual-type (https://github.com/palantir/tslint/issues/2428) const ALWAYS_OR_NEVER = { enum: ["always", "never"], type: "string", diff --git a/src/rules/trailingCommaRule.ts b/src/rules/trailingCommaRule.ts index 55932a720eb..b37c640d072 100644 --- a/src/rules/trailingCommaRule.ts +++ b/src/rules/trailingCommaRule.ts @@ -47,7 +47,7 @@ function normalizeOptions(options: OptionsJson): Options { } } -/* tslint:disable:object-literal-sort-keys */ +/* tslint:disable:object-literal-sort-keys object-literal-contextual-type */ const metadataOptionShape = { anyOf: [{ type: "string", @@ -60,7 +60,7 @@ const metadataOptionShape = { }), }], }; -/* tslint:enable:object-literal-sort-keys */ +/* tslint:enable:object-literal-sort-keys object-literal-contextual-type */ export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ diff --git a/src/rules/typedefWhitespaceRule.ts b/src/rules/typedefWhitespaceRule.ts index 9b0291b11f1..db14979ba97 100644 --- a/src/rules/typedefWhitespaceRule.ts +++ b/src/rules/typedefWhitespaceRule.ts @@ -19,7 +19,7 @@ import * as ts from "typescript"; import * as Lint from "../index"; -/* tslint:disable:object-literal-sort-keys */ +/* tslint:disable:object-literal-sort-keys object-literal-contextual-type (https://github.com/palantir/tslint/issues/2428) */ const SPACE_OPTIONS = { type: "string", enum: ["nospace", "onespace", "space"], diff --git a/src/rules/unifiedSignaturesRule.ts b/src/rules/unifiedSignaturesRule.ts index e8fec173292..31c4d3f8c46 100644 --- a/src/rules/unifiedSignaturesRule.ts +++ b/src/rules/unifiedSignaturesRule.ts @@ -100,14 +100,13 @@ class Walker extends Lint.RuleWalker { } private checkMembers(members: Array, typeParameters?: ts.TypeParameterDeclaration[]) { - this.checkOverloads(members, getOverloadName, typeParameters); - function getOverloadName(member: ts.TypeElement | ts.ClassElement) { + this.checkOverloads(members, (member) => { if (!utils.isSignatureDeclaration(member) || (member as ts.MethodDeclaration).body) { return undefined; } const key = getOverloadKey(member); return key === undefined ? undefined : { signature: member, key }; - } + }, typeParameters); } private checkOverloads(signatures: T[], getOverload: GetOverload, typeParameters?: ts.TypeParameterDeclaration[]) { diff --git a/src/test.ts b/src/test.ts index d540d59dca9..dc2208cc675 100644 --- a/src/test.ts +++ b/src/test.ts @@ -23,6 +23,7 @@ import * as path from "path"; import * as semver from "semver"; import * as ts from "typescript"; +import {ILinterOptions} from "../src"; import {Replacement} from "./language/rule/rule"; import * as Linter from "./linter"; import {LintError} from "./test/lintError"; @@ -76,7 +77,7 @@ export function runTest(testDirectory: string, rulesDirectory?: string | string[ throw new Error(JSON.stringify(error)); } - const parseConfigHost = { + const parseConfigHost: ts.ParseConfigHost = { fileExists: fs.existsSync, readDirectory: ts.sys.readDirectory, readFile: (file: string) => fs.readFileSync(file, "utf8"), @@ -144,7 +145,7 @@ export function runTest(testDirectory: string, rulesDirectory?: string | string[ ts.getPreEmitDiagnostics(program); } - const lintOptions = { + const lintOptions: ILinterOptions = { fix: false, formatter: "prose", formattersDirectory: "", diff --git a/src/test/parse.ts b/src/test/parse.ts index 9a0ca093aa3..c5146369817 100644 --- a/src/test/parse.ts +++ b/src/test/parse.ts @@ -27,7 +27,7 @@ import { parseLine, printLine, } from "./lines"; -import {errorComparator, LintError, lintSyntaxError} from "./lintError"; +import {errorComparator, LintError, lintSyntaxError, PositionInFile} from "./lintError"; let scanner: ts.Scanner | undefined; @@ -74,7 +74,7 @@ export function parseErrorsFromMarkup(text: string): LintError[] { const errorLinesForCodeLines = createCodeLineNoToErrorsMap(lines); const lintErrors: LintError[] = []; - function addError(errorLine: EndErrorLine, errorStartPos: { line: number, col: number }, lineNo: number) { + function addError(errorLine: EndErrorLine, errorStartPos: PositionInFile, lineNo: number) { lintErrors.push({ startPos: errorStartPos, endPos: { line: lineNo, col: errorLine.endCol }, @@ -87,7 +87,7 @@ export function parseErrorsFromMarkup(text: string): LintError[] { // for each error marking on that line... while (errorLinesForLineOfCode.length > 0) { const errorLine = errorLinesForLineOfCode.shift(); - const errorStartPos = { line: lineNo, col: errorLine!.startCol }; + const errorStartPos: PositionInFile = { line: lineNo, col: errorLine!.startCol }; // if the error starts and ends on this line, add it now to list of errors if (errorLine instanceof EndErrorLine) { diff --git a/test/configurationTests.ts b/test/configurationTests.ts index 170cd89070b..38be4f331da 100644 --- a/test/configurationTests.ts +++ b/test/configurationTests.ts @@ -39,7 +39,7 @@ describe("Configuration", () => { }); it("arrayifies `extends`", () => { - const rawConfig = { + const rawConfig: RawConfigFile = { extends: "a", }; const expected = getEmptyConfig(); diff --git a/test/formatters/jsonFormatterTests.ts b/test/formatters/jsonFormatterTests.ts index a69e3ea4b02..d97cded69ce 100644 --- a/test/formatters/jsonFormatterTests.ts +++ b/test/formatters/jsonFormatterTests.ts @@ -42,7 +42,7 @@ describe("JSON Formatter", () => { "error"), ]; - /* tslint:disable:object-literal-sort-keys */ + /* tslint:disable object-literal-sort-keys */ const expectedResult: IRuleFailureJson[] = [{ name: TEST_FILE, failure: "first failure", @@ -96,7 +96,7 @@ describe("JSON Formatter", () => { ruleName: "full-name", ruleSeverity: "ERROR", }]; - /* tslint:enable:object-literal-sort-keys */ + /* tslint:enable:object-literal-sort-keys object-literal-contextual-type */ const actualResult = JSON.parse(formatter.format(failures)); assert.deepEqual(actualResult, expectedResult); diff --git a/test/rules/object-literal-contextual-type/test.ts.lint b/test/rules/object-literal-contextual-type/test.ts.lint new file mode 100644 index 00000000000..c02db2c9930 --- /dev/null +++ b/test/rules/object-literal-contextual-type/test.ts.lint @@ -0,0 +1,25 @@ +const ok: { x: 0 } = { x: 0 }; + +const notOk = { x: 0 }; + ~~~~~~~~ [0] + +// `{}` OK. +const empty = {}; + +declare function f(obj: { x: number }): void; +f({ x: 0 }); + +// Allow any literal of type 'any', +// or nested inside an array or object literal in something else of type 'any'. +const isAny: any = { + a: { x: 0 }, + b: [ + { x: 0 }, + ], + c() { + return { x: 0 }; + ~~~~~~~~ [0] + }, +}; + +[0]: Object literal has no contextual type. diff --git a/test/rules/object-literal-contextual-type/tslint.json b/test/rules/object-literal-contextual-type/tslint.json new file mode 100644 index 00000000000..fbf6586829a --- /dev/null +++ b/test/rules/object-literal-contextual-type/tslint.json @@ -0,0 +1,8 @@ +{ + "linterOptions": { + "typeCheck": true + }, + "rules": { + "object-literal-contextual-type": true + } +} diff --git a/test/utilsTests.ts b/test/utilsTests.ts index 5ad9df80de0..08d2e009e4b 100644 --- a/test/utilsTests.ts +++ b/test/utilsTests.ts @@ -34,6 +34,7 @@ describe("Utils", () => { assert.deepEqual(objectify(null), {}); assert.deepEqual(objectify("foo"), {}); assert.deepEqual(objectify(1), {}); + // tslint:disable-next-line object-literal-contextual-type assert.deepEqual(objectify({foo: 1, mar: {baz: 2}}), {foo: 1, mar: {baz: 2}}); }); diff --git a/tslint.json b/tslint.json index c679e195f00..b55a0dc9bf9 100644 --- a/tslint.json +++ b/tslint.json @@ -28,6 +28,7 @@ "no-string-throw": true, "no-switch-case-fall-through": true, "no-unsafe-any": true, + "object-literal-contextual-type": true, "prefer-const": true, "switch-default": false, "variable-name": [true,