Skip to content

Commit 3847f76

Browse files
committed
feat: optimize-string-ternary
1 parent 393dc9a commit 3847f76

File tree

6 files changed

+206
-24
lines changed

6 files changed

+206
-24
lines changed

docs/rules/optimize-string-ternary.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
title: optimize-string-ternary
3+
description: Disallow repetition when building strings with ternaries=
4+
---
5+
6+
<script setup lang="ts">
7+
import CodeEditor from '../../.vitepress/theme/components/code-editor.vue';
8+
import {ruleName, presetConfigs, initialText} from '../../src/sample-code/optimize-string-ternary';
9+
</script>
10+
11+
> Duplication is the primary enemy of a well-designed system. — Robert C. Martin
12+
13+
# Disallow repetition when building strings with ternaries (`optimize-string-ternary`)
14+
15+
💼 This rule is enabled in the following [configs](/configs/): 🌐 `all`, ✅
16+
`recommended`.
17+
18+
🔧 This rule is automatically fixable by the
19+
[`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
20+
21+
<!-- end auto-generated rule header -->
22+
23+
## 📖 Rule details
24+
25+
This rule disallows repetition when building strings with ternaries.
26+
27+
## 💡 Examples
28+
29+
```js
30+
// ❌ Incorrect
31+
const CauseOrCampaign = !isNotCause ? 'Cause' : 'Campaign'
32+
33+
// ✅ Correct
34+
const CauseOrCampaign = 'Ca' + (!isNotCause ? 'use' : 'mpaign')
35+
```
36+
37+
## 🔧 Config
38+
39+
```js
40+
{ rules: { 'ninja/optimize-string-ternary': 2 } }
41+
```
42+
43+
## 🧑‍💻 Demo
44+
45+
<CodeEditor :rule="ruleName" :text="initialText" :presetConfigs="presetConfigs" />

index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import optimiseStringTernary, { RULE_NAME as optimiseStringTernaryName } from './rules/optimize-string-ternary'
12
import declareKeyword, { RULE_NAME as declareKeywordName } from './rules/declare-keyword'
23
import noUselessFor, { RULE_NAME as noUselessForName } from './rules/no-avoidable-loop'
34
import noNoPlusPlus, { RULE_NAME as noNoPlusPlusName } from './rules/no-no-plusplus'
@@ -40,6 +41,7 @@ const rules = {
4041
[noUselessForName]: noUselessFor,
4142
[noWoofName]: noWoof,
4243
[noXkcdName]: noXkcd,
44+
[optimiseStringTernaryName]: optimiseStringTernary,
4345
[preferEmojiName]: preferEmoji,
4446
[preferNpmName]: preferNpm,
4547
[preferTabName]: preferTab,
@@ -61,6 +63,7 @@ const recommendedRules = [
6163
noUselessForName,
6264
noWoofName,
6365
noXkcdName,
66+
optimiseStringTernaryName,
6467
preferEmojiName,
6568
preferNpmName,
6669
yesName,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "eslint-plugin-ninja",
33
"description": "Optimized ESLint Rules for Elite Coders",
4-
"version": "0.0.6",
4+
"version": "0.0.7",
55
"homepage": "https://www.dont.ninja",
66
"repository": {
77
"type": "git",

readme.md

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,29 +45,30 @@ npm i -D eslint-plugin-ninja
4545
🔧 Automatically fixable by the
4646
[`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
4747

48-
| Name | Description | 🔧 |
49-
| :------------------------------------------------------------------ | :-------------------------------------------------------- | :-- |
50-
| [align](https://www.dont.ninja/rules/align) | enforce elegant text alignment | 🔧 |
51-
| [declare-keyword](https://www.dont.ninja/rules/declare-keyword) | enforce variable names to include their type | 🔧 |
52-
| [justify](https://www.dont.ninja/rules/justify) | enforce comments explaining code | 🔧 |
53-
| [justify2](https://www.dont.ninja/rules/justify2) | enforce text to be justified | 🔧 |
54-
| [lottery](https://www.dont.ninja/rules/lottery) | require luck | |
55-
| [monopoly](https://www.dont.ninja/rules/monopoly) | disallow bad eslint configs | 🔧 |
56-
| [no](https://www.dont.ninja/rules/no) | disallow everything | |
57-
| [no-avoidable-loop](https://www.dont.ninja/rules/no-avoidable-loop) | disallow useless `for` loops | 🔧 |
58-
| [no-ci](https://www.dont.ninja/rules/no-ci) | disallow running on CI lol | |
59-
| [no-no-plusplus](https://www.dont.ninja/rules/no-no-plusplus) | enforce the unary operators ++ and -- | 🔧 |
60-
| [no-object](https://www.dont.ninja/rules/no-object) | disallow object literals, prefer Map | 🔧 |
61-
| [no-overtime](https://www.dont.ninja/rules/no-overtime) | disallow overwork | |
62-
| [no-random](https://www.dont.ninja/rules/no-random) | disallow non-deterministic randomness | 🔧 |
63-
| [no-rush](https://www.dont.ninja/rules/no-rush) | enforce a delay | |
64-
| [no-ts](https://www.dont.ninja/rules/no-ts) | disallow gymnastics needed to please the TS compiler | 🔧 |
65-
| [no-woof](https://www.dont.ninja/rules/no-woof) | disallow woof! | 🔧 |
66-
| [no-xkcd](https://www.dont.ninja/rules/no-xkcd) | disallow xkcd references | 🔧 |
67-
| [prefer-emoji](https://www.dont.ninja/rules/prefer-emoji) | require variables and properties to be named using emojis | 🔧 |
68-
| [prefer-npm](https://www.dont.ninja/rules/prefer-npm) | require from npm instead of using JS builtins | 🔧 |
69-
| [prefer-tab](https://www.dont.ninja/rules/prefer-tab) | require word separators to be tabs, not spaces | 🔧 |
70-
| [yes](https://www.dont.ninja/rules/yes) | enforce nothing | |
48+
| Name | Description | 🔧 |
49+
| :------------------------------------------------------------------------------ | :-------------------------------------------------------- | :-- |
50+
| [align](https://www.dont.ninja/rules/align) | enforce elegant text alignment | 🔧 |
51+
| [declare-keyword](https://www.dont.ninja/rules/declare-keyword) | enforce variable names to include their type | 🔧 |
52+
| [justify](https://www.dont.ninja/rules/justify) | enforce comments explaining code | 🔧 |
53+
| [justify2](https://www.dont.ninja/rules/justify2) | enforce text to be justified | 🔧 |
54+
| [lottery](https://www.dont.ninja/rules/lottery) | require luck | |
55+
| [monopoly](https://www.dont.ninja/rules/monopoly) | disallow bad eslint configs | 🔧 |
56+
| [no](https://www.dont.ninja/rules/no) | disallow everything | |
57+
| [no-avoidable-loop](https://www.dont.ninja/rules/no-avoidable-loop) | disallow useless `for` loops | 🔧 |
58+
| [no-ci](https://www.dont.ninja/rules/no-ci) | disallow running on CI lol | |
59+
| [no-no-plusplus](https://www.dont.ninja/rules/no-no-plusplus) | enforce the unary operators ++ and -- | 🔧 |
60+
| [no-object](https://www.dont.ninja/rules/no-object) | disallow object literals, prefer Map | 🔧 |
61+
| [no-overtime](https://www.dont.ninja/rules/no-overtime) | disallow overwork | |
62+
| [no-random](https://www.dont.ninja/rules/no-random) | disallow non-deterministic randomness | 🔧 |
63+
| [no-rush](https://www.dont.ninja/rules/no-rush) | enforce a delay | |
64+
| [no-ts](https://www.dont.ninja/rules/no-ts) | disallow gymnastics needed to please the TS compiler | 🔧 |
65+
| [no-woof](https://www.dont.ninja/rules/no-woof) | disallow woof! | 🔧 |
66+
| [no-xkcd](https://www.dont.ninja/rules/no-xkcd) | disallow xkcd references | 🔧 |
67+
| [optimize-string-ternary](https://www.dont.ninja/rules/optimize-string-ternary) | disallow repetition when building strings with ternaries | 🔧 |
68+
| [prefer-emoji](https://www.dont.ninja/rules/prefer-emoji) | require variables and properties to be named using emojis | 🔧 |
69+
| [prefer-npm](https://www.dont.ninja/rules/prefer-npm) | require from npm instead of using JS builtins | 🔧 |
70+
| [prefer-tab](https://www.dont.ninja/rules/prefer-tab) | require word separators to be tabs, not spaces | 🔧 |
71+
| [yes](https://www.dont.ninja/rules/yes) | enforce nothing | |
7172

7273
<!-- end auto-generated rules list -->
7374

rules/optimize-string-ternary.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import type { RuleContext, RuleListener } from '../utils/eslint-types/Rule'
2+
3+
import { createEslintRule } from '../utils/create-eslint-rule'
4+
5+
type MESSAGE_ID = 'message'
6+
7+
type Options = []
8+
9+
export const RULE_NAME = 'optimize-string-ternary'
10+
11+
type Context = RuleContext<MESSAGE_ID, Options>
12+
13+
export default createEslintRule<Options, MESSAGE_ID>({
14+
name: RULE_NAME,
15+
meta: {
16+
type: 'problem',
17+
docs: {
18+
description: 'disallow repetition when building strings with ternaries',
19+
},
20+
fixable: 'code',
21+
schema: [
22+
{
23+
type: 'object',
24+
properties: {},
25+
},
26+
],
27+
messages: {
28+
message: 'Repetition in string ternary expressions should be avoided',
29+
},
30+
},
31+
defaultOptions: [],
32+
create: (context: Context): RuleListener => ({
33+
ConditionalExpression: node => {
34+
const { consequent, alternate } = node
35+
36+
if (
37+
consequent.type === 'Literal' &&
38+
alternate.type === 'Literal' &&
39+
typeof consequent.value === 'string' &&
40+
typeof alternate.value === 'string'
41+
) {
42+
if (consequent.value === alternate.value) return null
43+
const commonPrefix = findCommonPrefix(consequent.value, alternate.value)
44+
const commonSuffix = findCommonSuffix(consequent.value, alternate.value)
45+
46+
if (commonPrefix.length > 0 && commonSuffix.length > 0) {
47+
return context.report({
48+
node,
49+
messageId: 'message',
50+
fix: fixer => {
51+
const newConsequent = consequent.value
52+
.slice(commonPrefix.length)
53+
.slice(0, -commonSuffix.length)
54+
const newAlternate = alternate.value
55+
.slice(commonPrefix.length)
56+
.slice(0, -commonSuffix.length)
57+
const newText = `'${commonPrefix}' + (${context.sourceCode.getText(
58+
node.test,
59+
)} ? '${newConsequent}' : '${newAlternate}') + '${commonSuffix}'`
60+
return fixer.replaceText(node, newText)
61+
},
62+
})
63+
}
64+
65+
if (commonPrefix.length > 0) {
66+
return context.report({
67+
node,
68+
messageId: 'message',
69+
fix: fixer => {
70+
const newConsequent = consequent.value.slice(commonPrefix.length)
71+
const newAlternate = alternate.value.slice(commonPrefix.length)
72+
const newText = `'${commonPrefix}' + (${context.sourceCode.getText(
73+
node.test,
74+
)} ? '${newConsequent}' : '${newAlternate}')`
75+
return fixer.replaceText(node, newText)
76+
},
77+
})
78+
}
79+
80+
if (commonSuffix.length > 0) {
81+
return context.report({
82+
node,
83+
messageId: 'message',
84+
fix: fixer => {
85+
const newConsequent = consequent.value.slice(
86+
0,
87+
-commonSuffix.length,
88+
)
89+
const newAlternate = alternate.value.slice(
90+
0,
91+
-commonSuffix.length,
92+
)
93+
const newText = `(${context.sourceCode.getText(
94+
node.test,
95+
)} ? '${newConsequent}' : '${newAlternate}') + '${commonSuffix}'`
96+
return fixer.replaceText(node, newText)
97+
},
98+
})
99+
}
100+
}
101+
102+
return null
103+
},
104+
}),
105+
})
106+
107+
const findCommonPrefix = (a: string, b: string) => {
108+
let prefix = ''
109+
for (let i = 0; i < Math.min(a.length, b.length); i++)
110+
if (a[i] === b[i]) prefix += a[i]
111+
else break
112+
return prefix
113+
}
114+
115+
const findCommonSuffix = (a: string, b: string) => {
116+
let suffix = ''
117+
for (let i = 0; i < Math.min(a.length, b.length); i++)
118+
if (a[a.length - 1 - i] === b[b.length - 1 - i])
119+
suffix = a[a.length - 1 - i] + suffix
120+
else break
121+
return suffix
122+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { PresetConfig } from './presets'
2+
3+
export const ruleName = 'optimize-string-ternary'
4+
5+
export const presetConfigs = [] satisfies PresetConfig[]
6+
7+
export const initialText = `const foo = (bool: boolean) => [
8+
bool ? 'Bottle' : 'Boat',
9+
bool ? 'Worst' : 'Best',
10+
bool ? 'Star Light' : 'Star Bright',
11+
]`

0 commit comments

Comments
 (0)