Skip to content

Commit 64082aa

Browse files
committed
feat(ts-no-unnecessary-template-expression): create new rule to catch extra markup within template types
1 parent a19af97 commit 64082aa

File tree

10 files changed

+533
-0
lines changed

10 files changed

+533
-0
lines changed

.README/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ Finally, enable all of the rules that you would like to use.
281281
"jsdoc/ts-prefer-function-type": 1,
282282
"jsdoc/ts-method-signature-style": 1,
283283
"jsdoc/ts-prefer-function-type": 1,
284+
"jsdoc/ts-no-unnecessary-template-expression": 1,
284285
"jsdoc/type-formatting": 1,
285286
"jsdoc/valid-types": 1 // Recommended
286287
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# `ts-no-unnecessary-template-expression`
2+
3+
Catches unnecessary template expressions such as string expressions within
4+
a template literal.
5+
6+
## Options
7+
8+
{"gitdown": "options"}
9+
10+
|||
11+
|---|---|
12+
|Context|everywhere|
13+
|Tags|``|
14+
|Recommended|false|
15+
|Settings||
16+
|Options|`enableFixer`|
17+
18+
## Failing examples
19+
20+
<!-- assertions-failing tsNoUnnecessaryTemplateExpression -->
21+
22+
## Passing examples
23+
24+
<!-- assertions-passing tsNoUnnecessaryTemplateExpression -->

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ Finally, enable all of the rules that you would like to use.
308308
"jsdoc/ts-prefer-function-type": 1,
309309
"jsdoc/ts-method-signature-style": 1,
310310
"jsdoc/ts-prefer-function-type": 1,
311+
"jsdoc/ts-no-unnecessary-template-expression": 1,
311312
"jsdoc/type-formatting": 1,
312313
"jsdoc/valid-types": 1 // Recommended
313314
}
@@ -502,6 +503,7 @@ non-default-recommended fixer).
502503
||:wrench:| [text-escaping](./docs/rules/text-escaping.md#readme) | Auto-escape certain characters that are input within block and tag descriptions. |
503504
||:wrench:| [ts-method-signature-style](./docs/rules/ts-method-signature-style.md#readme) | Prefers either function properties or method signatures |
504505
|:heavy_check_mark:|| [ts-no-empty-object-type](./docs/rules/ts-no-empty-object-type.md#readme) | Warns against use of the empty object type |
506+
||:wrench:| [ts-no-unnecessary-template-expression](./docs/rules/ts-no-unnecessary-template-expression.md#readme) | Catches unnecessary template expressions such as string expressions within a template literal. |
505507
||:wrench:| [ts-prefer-function-type](./docs/rules/ts-prefer-function-type.md#readme) | Prefers function types over call signatures when there are no other properties. |
506508
||:wrench:| [type-formatting](./docs/rules/type-formatting.md#readme) | Formats JSDoc type values. |
507509
|:heavy_check_mark:|| [valid-types](./docs/rules/valid-types.md#readme) | Requires all types/namepaths to be valid JSDoc, Closure compiler, or TypeScript types (configurable in settings). |
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<a name="user-content-ts-no-unnecessary-template-expression"></a>
2+
<a name="ts-no-unnecessary-template-expression"></a>
3+
# <code>ts-no-unnecessary-template-expression</code>
4+
5+
Catches unnecessary template expressions such as string expressions within
6+
a template literal.
7+
8+
<a name="user-content-ts-no-unnecessary-template-expression-options"></a>
9+
<a name="ts-no-unnecessary-template-expression-options"></a>
10+
## Options
11+
12+
A single options object has the following properties.
13+
14+
<a name="user-content-ts-no-unnecessary-template-expression-options-enablefixer"></a>
15+
<a name="ts-no-unnecessary-template-expression-options-enablefixer"></a>
16+
### <code>enableFixer</code>
17+
18+
Whether to enable the fixer. Defaults to `true`.
19+
20+
21+
|||
22+
|---|---|
23+
|Context|everywhere|
24+
|Tags|``|
25+
|Recommended|false|
26+
|Settings||
27+
|Options|`enableFixer`|
28+
29+
<a name="user-content-ts-no-unnecessary-template-expression-failing-examples"></a>
30+
<a name="ts-no-unnecessary-template-expression-failing-examples"></a>
31+
## Failing examples
32+
33+
The following patterns are considered problems:
34+
35+
````ts
36+
/**
37+
* @type {`A${'B'}`}
38+
*/
39+
// Message: Found an unnecessary string literal within a template.
40+
41+
/**
42+
* @type {`A${'B'}`}
43+
*/
44+
// "jsdoc/ts-no-unnecessary-template-expression": ["error"|"warn", {"enableFixer":false}]
45+
// Message: Found an unnecessary string literal within a template.
46+
47+
/**
48+
* @type {(`A${'B'}`)}
49+
*/
50+
// Message: Found an unnecessary string literal within a template.
51+
52+
/**
53+
* @type {`A${'B'}`|SomeType}
54+
*/
55+
// Message: Found an unnecessary string literal within a template.
56+
57+
/**
58+
* @type {`${B}`}
59+
*/
60+
// Message: Found a lone template expression within a template.
61+
62+
/**
63+
* @type {`${B}`}
64+
*/
65+
// "jsdoc/ts-no-unnecessary-template-expression": ["error"|"warn", {"enableFixer":false}]
66+
// Message: Found a lone template expression within a template.
67+
68+
/**
69+
* @type {`A${'B'}${'C'}`}
70+
*/
71+
// Message: Found an unnecessary string literal within a template.
72+
73+
/**
74+
* @type {`A${'B'}C`}
75+
*/
76+
// Message: Found an unnecessary string literal within a template.
77+
78+
/**
79+
* @type {`${'B'}`}
80+
*/
81+
// Message: Found an unnecessary string literal within a template.
82+
83+
/**
84+
* @type {(`${B}`)}
85+
*/
86+
// Message: Found a lone template expression within a template.
87+
88+
/**
89+
* @type {`${B}` | number}
90+
*/
91+
// Message: Found a lone template expression within a template.
92+
````
93+
94+
95+
96+
<a name="user-content-ts-no-unnecessary-template-expression-passing-examples"></a>
97+
<a name="ts-no-unnecessary-template-expression-passing-examples"></a>
98+
## Passing examples
99+
100+
The following patterns are not considered problems:
101+
102+
````ts
103+
/**
104+
* @type {`AB`}
105+
*/
106+
107+
/**
108+
* @type {`A${C}B`}
109+
*/
110+
111+
/**
112+
* @param {BadType<} someName
113+
*/
114+
115+
/**
116+
* @param {`A${'B'}`} someName
117+
*/
118+
// Settings: {"jsdoc":{"mode":"jsdoc"}}
119+
````
120+

src/index-cjs.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import tagLines from './rules/tagLines.js';
6868
import textEscaping from './rules/textEscaping.js';
6969
import tsMethodSignatureStyle from './rules/tsMethodSignatureStyle.js';
7070
import tsNoEmptyObjectType from './rules/tsNoEmptyObjectType.js';
71+
import tsNoUnnecessaryTemplateExpression from './rules/tsNoUnnecessaryTemplateExpression.js';
7172
import tsPreferFunctionType from './rules/tsPreferFunctionType.js';
7273
import typeFormatting from './rules/typeFormatting.js';
7374
import validTypes from './rules/validTypes.js';
@@ -256,6 +257,7 @@ index.rules = {
256257
'text-escaping': textEscaping,
257258
'ts-method-signature-style': tsMethodSignatureStyle,
258259
'ts-no-empty-object-type': tsNoEmptyObjectType,
260+
'ts-no-unnecessary-template-expression': tsNoUnnecessaryTemplateExpression,
259261
'ts-prefer-function-type': tsPreferFunctionType,
260262
'type-formatting': typeFormatting,
261263
'valid-types': validTypes,
@@ -349,6 +351,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => {
349351
'jsdoc/text-escaping': 'off',
350352
'jsdoc/ts-method-signature-style': 'off',
351353
'jsdoc/ts-no-empty-object-type': warnOrError,
354+
'jsdoc/ts-no-unnecessary-template-expression': 'off',
352355
'jsdoc/ts-prefer-function-type': 'off',
353356
'jsdoc/type-formatting': 'off',
354357
'jsdoc/valid-types': warnOrError,

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import tagLines from './rules/tagLines.js';
7474
import textEscaping from './rules/textEscaping.js';
7575
import tsMethodSignatureStyle from './rules/tsMethodSignatureStyle.js';
7676
import tsNoEmptyObjectType from './rules/tsNoEmptyObjectType.js';
77+
import tsNoUnnecessaryTemplateExpression from './rules/tsNoUnnecessaryTemplateExpression.js';
7778
import tsPreferFunctionType from './rules/tsPreferFunctionType.js';
7879
import typeFormatting from './rules/typeFormatting.js';
7980
import validTypes from './rules/validTypes.js';
@@ -262,6 +263,7 @@ index.rules = {
262263
'text-escaping': textEscaping,
263264
'ts-method-signature-style': tsMethodSignatureStyle,
264265
'ts-no-empty-object-type': tsNoEmptyObjectType,
266+
'ts-no-unnecessary-template-expression': tsNoUnnecessaryTemplateExpression,
265267
'ts-prefer-function-type': tsPreferFunctionType,
266268
'type-formatting': typeFormatting,
267269
'valid-types': validTypes,
@@ -355,6 +357,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => {
355357
'jsdoc/text-escaping': 'off',
356358
'jsdoc/ts-method-signature-style': 'off',
357359
'jsdoc/ts-no-empty-object-type': warnOrError,
360+
'jsdoc/ts-no-unnecessary-template-expression': 'off',
358361
'jsdoc/ts-prefer-function-type': 'off',
359362
'jsdoc/type-formatting': 'off',
360363
'jsdoc/valid-types': warnOrError,

src/rules.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2955,6 +2955,18 @@ export interface Rules {
29552955
/** Warns against use of the empty object type */
29562956
"jsdoc/ts-no-empty-object-type": [];
29572957

2958+
/** Catches unnecessary template expressions such as string expressions within a template literal. */
2959+
"jsdoc/ts-no-unnecessary-template-expression":
2960+
| []
2961+
| [
2962+
{
2963+
/**
2964+
* Whether to enable the fixer. Defaults to `true`.
2965+
*/
2966+
enableFixer?: boolean;
2967+
}
2968+
];
2969+
29582970
/** Prefers function types over call signatures when there are no other properties. */
29592971
"jsdoc/ts-prefer-function-type":
29602972
| []
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import iterateJsdoc from '../iterateJsdoc.js';
2+
import {
3+
rewireByParsedType,
4+
} from '../jsdocUtils.js';
5+
import {
6+
parse as parseType,
7+
traverse,
8+
} from '@es-joy/jsdoccomment';
9+
10+
export default iterateJsdoc(({
11+
context,
12+
indent,
13+
jsdoc,
14+
settings,
15+
utils,
16+
}) => {
17+
if (settings.mode !== 'typescript') {
18+
return;
19+
}
20+
21+
const {
22+
enableFixer = true,
23+
} = context.options[0] ?? {};
24+
25+
/**
26+
* @param {import('@es-joy/jsdoccomment').JsdocTagWithInline} tag
27+
*/
28+
const checkType = (tag) => {
29+
const potentialType = tag.type;
30+
/** @type {import('jsdoc-type-pratt-parser').RootResult} */
31+
let parsedType;
32+
try {
33+
parsedType = parseType(
34+
/** @type {string} */ (potentialType), 'typescript',
35+
);
36+
} catch {
37+
return;
38+
}
39+
40+
traverse(parsedType, (nde, parentNode, property, index) => {
41+
switch (nde.type) {
42+
case 'JsdocTypeTemplateLiteral': {
43+
const stringInterpolationIndex = nde.interpolations.findIndex((interpolation) => {
44+
return interpolation.type === 'JsdocTypeStringValue';
45+
});
46+
if (stringInterpolationIndex > -1) {
47+
utils.reportJSDoc(
48+
'Found an unnecessary string literal within a template.',
49+
tag,
50+
enableFixer ? () => {
51+
nde.literals.splice(
52+
stringInterpolationIndex,
53+
2,
54+
nde.literals[stringInterpolationIndex] +
55+
/** @type {import('jsdoc-type-pratt-parser').StringValueResult} */
56+
(nde.interpolations[stringInterpolationIndex]).value +
57+
nde.literals[stringInterpolationIndex + 1],
58+
);
59+
60+
nde.interpolations.splice(
61+
stringInterpolationIndex, 1,
62+
);
63+
64+
rewireByParsedType(jsdoc, tag, parsedType, indent);
65+
} : null,
66+
);
67+
} else if (nde.literals.length === 2 && nde.literals[0] === '' &&
68+
nde.literals[1] === ''
69+
) {
70+
utils.reportJSDoc(
71+
'Found a lone template expression within a template.',
72+
tag,
73+
enableFixer ? () => {
74+
const interpolation = nde.interpolations[0];
75+
76+
if (parentNode && property) {
77+
if (typeof index === 'number') {
78+
// @ts-expect-error Safe
79+
parentNode[property][index] = interpolation;
80+
} else {
81+
// @ts-expect-error Safe
82+
parentNode[property] = interpolation;
83+
}
84+
} else {
85+
parsedType = interpolation;
86+
}
87+
88+
rewireByParsedType(jsdoc, tag, parsedType, indent);
89+
} : null,
90+
);
91+
}
92+
}
93+
}
94+
});
95+
};
96+
97+
const tags = utils.filterTags(({
98+
tag,
99+
}) => {
100+
return Boolean(tag !== 'import' && utils.tagMightHaveTypePosition(tag));
101+
});
102+
103+
for (const tag of tags) {
104+
if (tag.type) {
105+
checkType(tag);
106+
}
107+
}
108+
}, {
109+
iterateAllJsdocs: true,
110+
meta: {
111+
docs: {
112+
description: 'Catches unnecessary template expressions such as string expressions within a template literal.',
113+
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/ts-no-unnecessary-template-expression.md#repos-sticky-header',
114+
},
115+
fixable: 'code',
116+
schema: [
117+
{
118+
additionalProperties: false,
119+
properties: {
120+
enableFixer: {
121+
description: 'Whether to enable the fixer. Defaults to `true`.',
122+
type: 'boolean',
123+
},
124+
},
125+
type: 'object',
126+
},
127+
],
128+
type: 'suggestion',
129+
},
130+
});

0 commit comments

Comments
 (0)