From 4117f169c4b7209967b4b441982604ee51ebb45d Mon Sep 17 00:00:00 2001 From: aervin_ Date: Wed, 10 Jan 2018 13:51:38 -0500 Subject: [PATCH] [new-rule] newline-per-chained-call (#3278) --- src/configs/all.ts | 1 + src/rules/newlinePerChainedCallRule.ts | 84 ++++++++++++ .../default/test.ts.lint | 120 ++++++++++++++++++ .../default/tslint.json | 5 + tslint.json | 1 + 5 files changed, 211 insertions(+) create mode 100644 src/rules/newlinePerChainedCallRule.ts create mode 100644 test/rules/newline-per-chained-call/default/test.ts.lint create mode 100644 test/rules/newline-per-chained-call/default/tslint.json diff --git a/src/configs/all.ts b/src/configs/all.ts index 8d7904928d6..63b14cae706 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -198,6 +198,7 @@ export const rules = { "match-default-export-name": true, "new-parens": true, "newline-before-return": true, + "newline-per-chained-call": true, "no-angle-bracket-type-assertion": true, "no-boolean-literal-compare": true, "no-consecutive-blank-lines": true, diff --git a/src/rules/newlinePerChainedCallRule.ts b/src/rules/newlinePerChainedCallRule.ts new file mode 100644 index 00000000000..39b7b99acb3 --- /dev/null +++ b/src/rules/newlinePerChainedCallRule.ts @@ -0,0 +1,84 @@ +/** + * @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 { + isCallExpression, + isElementAccessExpression, + isPropertyAccessExpression, + isSameLine, +} from "tsutils"; +import * as ts from "typescript"; +import * as Lint from ".."; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "newline-per-chained-call", + description: Lint.Utils.dedent` + Requires that chained method calls be broken apart onto separate lines.`, + rationale: Lint.Utils.dedent` + This style helps to keep code 'vertical', avoiding the need for side-scrolling in IDEs or text editors.`, + optionsDescription: "Not configurable", + options: null, + type: "style", + typescriptOnly: false, + }; + /* tslint:enable:object-literal-sort-keys */ + public static FAILURE_STRING = "When chaining calls, put method calls on new lines."; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker( + new NewlinePerChainedCallWalker(sourceFile, this.ruleName, undefined), + ); + } +} + +class NewlinePerChainedCallWalker extends Lint.AbstractWalker { + public walk(sourceFile: ts.SourceFile) { + const checkForSameLine = (node: ts.Node): void => { + if ( + isCallExpression(node) && + isPropertyAccessExpression(node.expression) && + isSameLine( + sourceFile, + node.expression.expression.end, + node.expression.name.pos, + ) && + hasChildCall(node.expression) + ) { + this.addFailure( + node.expression.name.pos - 1, + node.expression.name.end, + Rule.FAILURE_STRING, + ); + } + return ts.forEachChild(node, checkForSameLine); + }; + return ts.forEachChild(sourceFile, checkForSameLine); + } +} + +function hasChildCall(node: ts.PropertyAccessExpression): boolean { + let { expression } = node; + while ( + isPropertyAccessExpression(expression) || + isElementAccessExpression(expression) + ) { + ({ expression } = expression); + } + return expression.kind === ts.SyntaxKind.CallExpression; +} diff --git a/test/rules/newline-per-chained-call/default/test.ts.lint b/test/rules/newline-per-chained-call/default/test.ts.lint new file mode 100644 index 00000000000..a00c7e99ea7 --- /dev/null +++ b/test/rules/newline-per-chained-call/default/test.ts.lint @@ -0,0 +1,120 @@ +this.getFoo()[0].toString(); + ~~~~~~~~~ [ERROR] + +this.foo()["bar"]().buzz(); + ~~~~~ [ERROR] + +this.foo()["bar"](); + +foo().bar(); + ~~~~ [ERROR] + +const y: string[] = _observable + .map(function(item) { return item.helloYay().another() } + ~~~~~~~~ [ERROR] + .operator() + .another(function(result) { return result.hello.Yay! }).wrong(); + ~~~~~~ [ERROR] + +SomeClass.propA.helloYay((a: number) => { + return a + 1; +}); + +this.some.nested(); + +const y: string[] = _observable + .map(function(item) { return item.helloYay! }) + .operator() + .another(function(result) { return result.hello.Yay! }); + + +const y: string[] = _observable.map(item => item.helloYay).operator().another(function(result) { return result.helloYay! }); + ~~~~~~~~ [ERROR] + ~~~~~~~~~ [ERROR] + +const x: string[] = _observable.map(item => item.helloYay); + +SomeClass.propA.propB.helloYay(); + +SomeClass + .propA + .propB + .helloYay(); + +SomeClass + .propA + .propB.helloYay(); + +SomeClass + .propA + .propB + .helloYay(function() { + return 1; + }).test(); + ~~~~~ [ERROR] + +SomeClass + .propA + .propB + .helloYay(function() { + return 1; + }). test(); + ~~~~~~~ [ERROR] + +SomeClass + .propA + .propB + .helloYay(function() { + return 1; + }). + ~ + test(); +~~~~~~~~ [ERROR] + +SomeClass.propA.propB.methodB(() => { + return "hello Yay!"; +}).helloYay((a: number) => { + ~~~~~~~~~ [ERROR] + return a + 1; +}); + +SomeClass.propA.propB.methodB(() => { + return "hello Yay!"; +}) +.helloYay((a: number) => { + return a + 1; +}); + +SomeClass.propA.propB.methodB(() => { + return "hello Yay!"; +}) +.helloYay((a: number) => { + return obj.method() + .chainedButOkay( + objB.nested().superNestedCall() + ~~~~~~~~~~~~~~~~ [ERROR] + ) +}); + +SomeClass.propA.propB.methodB(() => { + return "hello Yay!"; +}) +.helloYay((a: number) => { + return obj.method() + .chainedButOkay( + objB.nested() + .superNestedCall() + ) +}); + +SomeClass + .propA + .propB + .methodC(() => { + return "hello Yay!"; + }) + .helloYay(() => { + return 1; + }); + +[ERROR]: When chaining calls, put method calls on new lines. \ No newline at end of file diff --git a/test/rules/newline-per-chained-call/default/tslint.json b/test/rules/newline-per-chained-call/default/tslint.json new file mode 100644 index 00000000000..5501b8f65cf --- /dev/null +++ b/test/rules/newline-per-chained-call/default/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "newline-per-chained-call": true + } +} diff --git a/tslint.json b/tslint.json index 002f6bcd674..20754704831 100644 --- a/tslint.json +++ b/tslint.json @@ -54,6 +54,7 @@ "static-before-instance", "variables-before-functions" ], + "newline-per-chained-call": false, "no-console": { "options": ["log"] },