diff --git a/src/rules/jsxNoBindRule.ts b/src/rules/jsxNoBindRule.ts index bb92bc9..31eb68f 100644 --- a/src/rules/jsxNoBindRule.ts +++ b/src/rules/jsxNoBindRule.ts @@ -16,8 +16,19 @@ */ import * as Lint from "tslint"; -import { isCallExpression, isJsxAttribute, isJsxExpression } from "tsutils"; +import { + isCallExpression, + isJsxAttribute, + isJsxExpression, + isJsxOpeningElement, + isJsxSelfClosingElement, +} from "tsutils"; import * as ts from "typescript"; +import { isDOMComponent } from "../utils"; + +interface IOption { + allowDOMComponent: boolean; +} export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ @@ -31,9 +42,16 @@ export class Rule extends Lint.Rules.AbstractRule { not a semantic one (it doesn't use the type checker). So it may \ have some rare false positives if you define your own .bind function \ and supply this as a parameter.`, - options: null, - optionsDescription: "", - optionExamples: ["true"], + options: { + type: "array", + items: { + type: "string", + enum: ["allow-dom-component"], + }, + }, + optionsDescription: Lint.Utils.dedent` + Whether to allow function binding with DOM Components`, + optionExamples: [`[true, ["allow-dom-component"]]`], type: "functionality", typescriptOnly: false, }; @@ -46,16 +64,26 @@ export class Rule extends Lint.Rules.AbstractRule { // An optional 3rd parameter allows you to pass in a parsed version // of this.ruleArguments. If used, it is preferred to parse it into // a more useful object than this.getOptions(). - return this.applyWithFunction(sourceFile, walk); + return this.applyWithFunction(sourceFile, walk, { + allowDOMComponent: this.ruleArguments.indexOf("allow-dom-component") !== -1, + }); } } -function walk(ctx: Lint.WalkContext): void { +function walk(ctx: Lint.WalkContext): void { + let currentTagElement: ts.JsxOpeningElement | ts.JsxSelfClosingElement; return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void { + if (isJsxOpeningElement(node) || isJsxSelfClosingElement(node)) { + currentTagElement = node; + } if (!isJsxAttribute(node)) { return ts.forEachChild(node, cb); } + if (ctx.options.allowDOMComponent && isDOMComponent(currentTagElement)) { + return; + } + const initializer = node.initializer; if (initializer === undefined || !isJsxExpression(initializer)) { return; diff --git a/src/rules/jsxNoLambdaRule.ts b/src/rules/jsxNoLambdaRule.ts index 6fabbcc..5c1e295 100644 --- a/src/rules/jsxNoLambdaRule.ts +++ b/src/rules/jsxNoLambdaRule.ts @@ -18,6 +18,11 @@ import * as Lint from "tslint"; import { isJsxAttribute, isJsxExpression } from "tsutils"; import * as ts from "typescript"; +import { isDOMComponent } from "../utils"; + +interface IOption { + allowDOMComponent: boolean; +} export class Rule extends Lint.Rules.AbstractRule { /* tslint:disable:object-literal-sort-keys */ @@ -29,9 +34,16 @@ export class Rule extends Lint.Rules.AbstractRule { ES2015 arrow syntax) inside the render call stack works against pure component \ rendering. When doing an equality check between two lambdas, React will always \ consider them unequal values and force the component to re-render more often than necessary.`, - options: null, - optionsDescription: "", - optionExamples: ["true"], + options: { + type: "array", + items: { + type: "string", + enum: ["allow-dom-component"], + }, + }, + optionsDescription: Lint.Utils.dedent` + Whether to allow anonymous function with DOM Components`, + optionExamples: [`[true, ["allow-dom-component"]]`], type: "functionality", typescriptOnly: false, }; @@ -40,12 +52,18 @@ export class Rule extends Lint.Rules.AbstractRule { public static FAILURE_STRING = "Lambdas are forbidden in JSX attributes due to their rendering performance impact"; public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithFunction(sourceFile, walk); + return this.applyWithFunction(sourceFile, walk, { + allowDOMComponent: this.ruleArguments.indexOf("allow-dom-component") !== -1, + }); } } -function walk(ctx: Lint.WalkContext) { +function walk(ctx: Lint.WalkContext) { + let currentTagElement: ts.JsxOpeningElement | ts.JsxSelfClosingElement; return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void { + if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) { + currentTagElement = node; + } // continue iterations until JsxAttribute will be found if (isJsxAttribute(node)) { const { initializer } = node; @@ -60,6 +78,10 @@ function walk(ctx: Lint.WalkContext) { return; } + if (ctx.options.allowDOMComponent && isDOMComponent(currentTagElement)) { + return; + } + const { expression } = initializer; if (expression !== undefined && isLambda(expression)) { return ctx.addFailureAtNode(expression, Rule.FAILURE_STRING); diff --git a/src/utils.ts b/src/utils.ts index aaf7498..b846753 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -46,6 +46,10 @@ export function getDeleteFixForSpaceBetweenTokens( } } +export function isDOMComponent(node: ts.JsxOpeningElement | ts.JsxSelfClosingElement): boolean { + return /^[a-z]/.test(node.tagName.getText()); +} + function getTotalCharCount(comments: ts.CommentRange[]) { return comments .map((comment) => comment.end - comment.pos) diff --git a/test/rules/jsx-no-bind/test.tsx.lint b/test/rules/jsx-no-bind/test.tsx.lint index 12728d6..91b2da5 100644 --- a/test/rules/jsx-no-bind/test.tsx.lint +++ b/test/rules/jsx-no-bind/test.tsx.lint @@ -1,18 +1,28 @@ function foo() { } -export const myButton = ( +export const domButton = ( ); +export const domButton2 = ( + +); + export const myButton2 = ( - + ); export const selector = ( diff --git a/test/rules/jsx-no-bind/tslint.json b/test/rules/jsx-no-bind/tslint.json index a4dc669..67434b3 100644 --- a/test/rules/jsx-no-bind/tslint.json +++ b/test/rules/jsx-no-bind/tslint.json @@ -1,5 +1,5 @@ { "rules": { - "jsx-no-bind": true + "jsx-no-bind": [true, "allow-dom-component"] } } diff --git a/test/rules/jsx-no-lambda/test.tsx.lint b/test/rules/jsx-no-lambda/test.tsx.lint index 78ae180..12c9b1f 100644 --- a/test/rules/jsx-no-lambda/test.tsx.lint +++ b/test/rules/jsx-no-lambda/test.tsx.lint @@ -1,12 +1,12 @@ export const myButton = ( - + ); const myButton2 = ( - +); + +const myDOMButton2 = ( +