Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

New Rule: param-property-in-constructor #1358

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions src/rules/noImproperConstructorParamUsageRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* @license
* Copyright 2014 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 * as Lint from "../lint";
import * as ts from "typescript";

export class Rule extends Lint.Rules.AbstractRule {
/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "no-improper-constructor-param-usage",
description: "Disallows constructor parameters to be accessed without the 'this.' prefix",
rationale: Lint.Utils.dedent`
This helps enforce consistancy.
When a constructor parameter is accessed without the 'this.' prefix, it is actually not acessing the same thing.
Example:
class Foo {
constructor(public num: number) {
this.num = 10;
num = 5; //this should be disallowed!
}
}
const f = new Foo();
alert(f.num); // will display 10`,
optionsDescription: "Not configurable.",
options: null,
optionExamples: ["true"],
type: "functionality",
};
/* tslint:enable:object-literal-sort-keys */

public static FAILURE_STRING_FACTORY = (paramName: string) => {
return `The constructor parameter '${paramName}' should only be accessed with the 'this' keyword: 'this.${paramName}'`;
}

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
const languageService = Lint.createLanguageService(sourceFile.fileName, sourceFile.getFullText());
return this.applyWithWalker(new NoImproperCtorParamUsageWalker(sourceFile, this.getOptions(), languageService));
}
}

export class NoImproperCtorParamUsageWalker extends Lint.RuleWalker {

private languageService: ts.LanguageService;

constructor(sourceFile: ts.SourceFile, options: Lint.IOptions, languageService: ts.LanguageService) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could just make this private languageService: ts.LanguageService if you want (funny considering what this rule does 😄 )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol, ugh why did I not notice this! 😆

super(sourceFile, options);
this.languageService = languageService;
}

public visitConstructorDeclaration(node: ts.ConstructorDeclaration) {
if (node.parameters && node.parameters.length > 0) {
const fileName = this.getSourceFile().fileName;
node.parameters.forEach((param: ts.ParameterDeclaration) => {
Copy link
Contributor

@jkillian jkillian Jul 20, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for-of usage is preferred when you don't need an index argument. (I think this is for performance reasons?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance and consistency (one iteration style)

this.validateParam(param, fileName);
});
}
super.visitConstructorDeclaration(node);
}

private validateParam(param: ts.ParameterDeclaration, fileName: string) {
const highlights = this.languageService.getDocumentHighlights(fileName, param.name.getStart(), [fileName]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only need to validate if the parameter is actually a parameter property, right? We check for that in another rule like this:

if (parameter.modifiers != null && parameter.modifiers.length > 0) {


if ((highlights !== null && highlights[0].highlightSpans.length > 0)) {
const paramName = param.name.getText();
highlights[0].highlightSpans.forEach((span: ts.HighlightSpan, idx: number) => {
// Ignore the 1st reference which is the actual parameter definition
if (idx !== 0 && !this.spanHasThisUsage(span, fileName)) {
const msg = Rule.FAILURE_STRING_FACTORY(paramName);
this.addFailure(this.createFailure(span.textSpan.start, span.textSpan.length, msg));
}
});
}
}

private spanHasThisUsage(span: ts.HighlightSpan, fileName: string) {
const endPos = span.textSpan.start + span.textSpan.length;
const nameSpanInfo = this.languageService.getNameOrDottedNameSpan(fileName, span.textSpan.start, endPos);

// If the difference between these two is 5 characters, then that account for the missing `this.`!
return nameSpanInfo.length - span.textSpan.length === 5;
}
}
1 change: 1 addition & 0 deletions src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"rules/noDuplicateVariableRule.ts",
"rules/noEmptyRule.ts",
"rules/noEvalRule.ts",
"rules/noImproperConstructorParamUsageRule.ts",
"rules/noInferrableTypesRule.ts",
"rules/noInternalModuleRule.ts",
"rules/noInvalidThisRule.ts",
Expand Down
48 changes: 48 additions & 0 deletions test/rules/no-improper-constructor-param-usage/test.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
class FailingExample1 {
constructor(fruit: string, public num: number, private animal: string) {
this.num = 10;
num = 5;
~~~ [The constructor parameter 'num' should only be accessed with the 'this' keyword: 'this.num']

animal = "aardvark";
~~~~~~ [The constructor parameter 'animal' should only be accessed with the 'this' keyword: 'this.animal']
this.animal = "skunk";

fruit = "banana";
~~~~~ [The constructor parameter 'fruit' should only be accessed with the 'this' keyword: 'this.fruit']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a bug to me, fruit is just a regular old parameter, so this should be allowed, right? (Also occurs below)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good point. i suppose this rule should only check params that have a public or private modifier on it, correct?

this.fruit = "persimmon";
}
}

class FailingExample2 {
constructor(thing: Object) {
const wow=thing.doTheThing()
~~~~~ [The constructor parameter 'thing' should only be accessed with the 'this' keyword: 'this.thing']
console.log(wow,thing.amazingDvdCollection);
~~~~~ [The constructor parameter 'thing' should only be accessed with the 'this' keyword: 'this.thing']

const pasta = thing.nested.makePasta();
~~~~~ [The constructor parameter 'thing' should only be accessed with the 'this' keyword: 'this.thing']

thing = {};
~~~~~ [The constructor parameter 'thing' should only be accessed with the 'this' keyword: 'this.thing']

this.thing.doSomethingElse();
}
}

class PassingExample1 {
constructor(a: string, private b: string, public c: string) {
this.a = "aaa";
this.b = "bbb";
this.c = "ccc";
}
}

class PassingExample2 {
constructor(a: Object, private b: Object, public c: Object) {
this.a.doThing();
this.b.doOtherThing();
this.c.doLastThing();
}
}
5 changes: 5 additions & 0 deletions test/rules/no-improper-constructor-param-usage/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"no-improper-constructor-param-usage": true
}
}
3 changes: 2 additions & 1 deletion test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@
"../src/rules/noDuplicateVariableRule.ts",
"../src/rules/noEmptyRule.ts",
"../src/rules/noEvalRule.ts",
"../src/rules/noImproperConstructorParamUsageRule.ts",
"../src/rules/noInferrableTypesRule.ts",
"../src/rules/noInternalModuleRule.ts",
"../src/rules/noInvalidThisRule.ts",
"../src/rules/noMergeableNamespaceRule.ts",
"../src/rules/noNamespaceRule.ts",
"../src/rules/noUnusedNewRule.ts",
"../src/rules/noNullKeywordRule.ts",
"../src/rules/noReferenceRule.ts",
"../src/rules/noRequireImportsRule.ts",
Expand All @@ -109,6 +109,7 @@
"../src/rules/noTrailingWhitespaceRule.ts",
"../src/rules/noUnreachableRule.ts",
"../src/rules/noUnusedExpressionRule.ts",
"../src/rules/noUnusedNewRule.ts",
"../src/rules/noUnusedVariableRule.ts",
"../src/rules/noUseBeforeDeclareRule.ts",
"../src/rules/noVarKeywordRule.ts",
Expand Down