diff --git a/src/rules/jsxCurlyBracePresenceRule.ts b/src/rules/jsxCurlyBracePresenceRule.ts new file mode 100644 index 0000000..33a799b --- /dev/null +++ b/src/rules/jsxCurlyBracePresenceRule.ts @@ -0,0 +1,109 @@ +/** + * @license + * Copyright 2019 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 "tslint"; +import { isJsxAttribute, isJsxExpression, isStringLiteral, isTextualLiteral } from "tsutils"; +import * as ts from "typescript"; + +const OPTION_ALWAYS = "always"; +const OPTION_NEVER = "never"; +const CURLY_PRESENCE_VALUES = [OPTION_ALWAYS, OPTION_NEVER]; +const CURLY_PRESENCE_OBJECT = { + enum: CURLY_PRESENCE_VALUES, + type: "string", +}; + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "jsx-curly-brace-presence", + description: "Enforce curly braces or disallow unnecessary curly braces in JSX props", + hasFix: true, + optionsDescription: Lint.Utils.dedent` +One of the following options may be provided under the "props" key: + +* \`"${OPTION_ALWAYS}"\` requires JSX attributes to have curly braces around string literal values +* \`"${OPTION_NEVER}"\` requires JSX attributes to NOT have curly braces around string literal values + +If no option is provided, "${OPTION_NEVER}" is chosen as default.`, + options: { + type: "object", + properties: { + props: CURLY_PRESENCE_OBJECT, + }, + }, + optionExamples: [ + `{ props: "${OPTION_ALWAYS}" }`, + `{ props: "${OPTION_NEVER}" }`, + ], + type: "style", + typescriptOnly: false, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_CURLY_BRACE_SUPERFLUOUS = "JSX attribute must NOT have curly braces around string literal"; + public static FAILURE_CURLY_BRACE_MISSING = "JSX attribute must have curly braces around string literal"; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + const option = Array.isArray(this.ruleArguments) ? this.ruleArguments[0] : undefined; + + return this.applyWithFunction(sourceFile, walk, option); + } +} + +function walk(ctx: Lint.WalkContext<{ props: string } | undefined>): void { + return ts.forEachChild(ctx.sourceFile, validateCurlyBraces); + + function validateCurlyBraces(node: ts.Node): void { + if (isJsxAttribute(node)) { + if (typeof ctx.options === "object" && ctx.options.props === OPTION_ALWAYS) { + validateCurlyBracesArePresent(node); + } else { + validateCurlyBracesAreNotPresent(node); + } + } + return ts.forEachChild(node, validateCurlyBraces); + } + + function validateCurlyBracesArePresent(node: ts.JsxAttribute) { + const { initializer } = node; + if (initializer !== undefined) { + const hasStringInitializer = initializer.kind === ts.SyntaxKind.StringLiteral; + if (hasStringInitializer) { + const fix = Lint.Replacement.replaceNode(initializer, `{${initializer.getText()}}`); + ctx.addFailureAtNode(initializer, Rule.FAILURE_CURLY_BRACE_MISSING, fix); + } + } + } + + function validateCurlyBracesAreNotPresent(node: ts.JsxAttribute) { + const { initializer } = node; + if (initializer !== undefined + && isJsxExpression(initializer) + && initializer.expression !== undefined) { + if (isStringLiteral(initializer.expression)) { + const stringLiteralWithoutCurlies: string = initializer.expression.getText(); + const fix = Lint.Replacement.replaceNode(initializer, stringLiteralWithoutCurlies); + ctx.addFailureAtNode(initializer, Rule.FAILURE_CURLY_BRACE_SUPERFLUOUS, fix); + } else if (isTextualLiteral(initializer.expression)) { + const textualLiteralContent = initializer.expression.text; + const fix = Lint.Replacement.replaceNode(initializer, `"${textualLiteralContent}"`); + ctx.addFailureAtNode(initializer, Rule.FAILURE_CURLY_BRACE_SUPERFLUOUS, fix); + } + } + } +} diff --git a/test/rules/jsx-curly-brace-presence/always/test.tsx.fix b/test/rules/jsx-curly-brace-presence/always/test.tsx.fix new file mode 100644 index 0000000..d8cbc68 --- /dev/null +++ b/test/rules/jsx-curly-brace-presence/always/test.tsx.fix @@ -0,0 +1,3 @@ +const e1 = (

some text

); + +const e2 = (

some text

); diff --git a/test/rules/jsx-curly-brace-presence/always/test.tsx.lint b/test/rules/jsx-curly-brace-presence/always/test.tsx.lint new file mode 100644 index 0000000..58a18a4 --- /dev/null +++ b/test/rules/jsx-curly-brace-presence/always/test.tsx.lint @@ -0,0 +1,6 @@ +const e1 = (

some text

); + +const e2 = (

some text

); + ~~~~~ [0] + +[0]: JSX attribute must have curly braces around string literal \ No newline at end of file diff --git a/test/rules/jsx-curly-brace-presence/always/tslint.json b/test/rules/jsx-curly-brace-presence/always/tslint.json new file mode 100644 index 0000000..e32bf4d --- /dev/null +++ b/test/rules/jsx-curly-brace-presence/always/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "jsx-curly-brace-presence": { "options": { "props": "always" } } + } +} diff --git a/test/rules/jsx-curly-brace-presence/default/test.tsx.fix b/test/rules/jsx-curly-brace-presence/default/test.tsx.fix new file mode 100644 index 0000000..f4b4831 --- /dev/null +++ b/test/rules/jsx-curly-brace-presence/default/test.tsx.fix @@ -0,0 +1 @@ +const e1 = (

some text

); diff --git a/test/rules/jsx-curly-brace-presence/default/test.tsx.lint b/test/rules/jsx-curly-brace-presence/default/test.tsx.lint new file mode 100644 index 0000000..ec56d91 --- /dev/null +++ b/test/rules/jsx-curly-brace-presence/default/test.tsx.lint @@ -0,0 +1,4 @@ +const e1 = (

some text

); + ~~~~~~~ [0] + +[0]: JSX attribute must NOT have curly braces around string literal \ No newline at end of file diff --git a/test/rules/jsx-curly-brace-presence/default/tslint.json b/test/rules/jsx-curly-brace-presence/default/tslint.json new file mode 100644 index 0000000..9874e42 --- /dev/null +++ b/test/rules/jsx-curly-brace-presence/default/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "jsx-curly-brace-presence": true + } +} diff --git a/test/rules/jsx-curly-brace-presence/never/test.tsx.fix b/test/rules/jsx-curly-brace-presence/never/test.tsx.fix new file mode 100644 index 0000000..5aba86b --- /dev/null +++ b/test/rules/jsx-curly-brace-presence/never/test.tsx.fix @@ -0,0 +1,7 @@ +const e1 = (

some text

); + +const e2 = (

some text

); + +const e3 = (

some text

); + +const e4 = (

some text

); diff --git a/test/rules/jsx-curly-brace-presence/never/test.tsx.lint b/test/rules/jsx-curly-brace-presence/never/test.tsx.lint new file mode 100644 index 0000000..e8acf19 --- /dev/null +++ b/test/rules/jsx-curly-brace-presence/never/test.tsx.lint @@ -0,0 +1,11 @@ +const e1 = (

some text

); + ~~~~~~~ [0] + +const e2 = (

some text

); + +const e3 = (

some text

); + +const e4 = (

some text

); + ~~~~~~~ [0] + +[0]: JSX attribute must NOT have curly braces around string literal \ No newline at end of file diff --git a/test/rules/jsx-curly-brace-presence/never/tslint.json b/test/rules/jsx-curly-brace-presence/never/tslint.json new file mode 100644 index 0000000..75dfdcd --- /dev/null +++ b/test/rules/jsx-curly-brace-presence/never/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "jsx-curly-brace-presence": { "options": { "props": "never" } } + } +}