Skip to content

Commit cbda8aa

Browse files
committed
feat(ts-prefer-function-type): add new rule
1 parent 92bf39e commit cbda8aa

File tree

9 files changed

+365
-0
lines changed

9 files changed

+365
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# `ts-prefer-function-type`
2+
3+
Inspired by `typescript-eslint`'s [prefer-function-type](https://typescript-eslint.io/rules/prefer-function-type/) rule.
4+
5+
Chooses the more succinct function property over a call signature if there are
6+
no other properties on the signature.
7+
8+
## Options
9+
10+
{"gitdown": "options"}
11+
12+
|||
13+
|---|---|
14+
|Context|everywhere|
15+
|Tags|``|
16+
|Recommended|false|
17+
|Settings||
18+
|Options|`enableFixer`|
19+
20+
## Failing examples
21+
22+
<!-- assertions-failing tsPreferFunctionType -->
23+
24+
## Passing examples
25+
26+
<!-- assertions-passing tsPreferFunctionType -->

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,5 +499,6 @@ non-default-recommended fixer).
499499
|:heavy_check_mark:|:wrench:| [tag-lines](./docs/rules/tag-lines.md#readme) | Enforces lines (or no lines) before, after, or between tags. |
500500
||:wrench:| [text-escaping](./docs/rules/text-escaping.md#readme) | Auto-escape certain characters that are input within block and tag descriptions. |
501501
||:wrench:| [ts-method-signature-style](./docs/rules/ts-method-signature-style.md#readme) | Prefers either function properties or method signatures |
502+
||:wrench:| [ts-prefer-function-type](./docs/rules/ts-prefer-function-type.md#readme) | |
502503
||:wrench:| [type-formatting](./docs/rules/type-formatting.md#readme) | Formats JSDoc type values. |
503504
|: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: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<a name="user-content-ts-prefer-function-type"></a>
2+
<a name="ts-prefer-function-type"></a>
3+
# <code>ts-prefer-function-type</code>
4+
5+
Inspired by `typescript-eslint`'s [prefer-function-type](https://typescript-eslint.io/rules/prefer-function-type/) rule.
6+
7+
Chooses the more succinct function property over a call signature if there are
8+
no other properties on the signature.
9+
10+
<a name="user-content-ts-prefer-function-type-options"></a>
11+
<a name="ts-prefer-function-type-options"></a>
12+
## Options
13+
14+
A single options object has the following properties.
15+
16+
<a name="user-content-ts-prefer-function-type-options-enablefixer"></a>
17+
<a name="ts-prefer-function-type-options-enablefixer"></a>
18+
### <code>enableFixer</code>
19+
20+
Whether to enable the fixer or not
21+
22+
23+
|||
24+
|---|---|
25+
|Context|everywhere|
26+
|Tags|``|
27+
|Recommended|false|
28+
|Settings||
29+
|Options|`enableFixer`|
30+
31+
<a name="user-content-ts-prefer-function-type-failing-examples"></a>
32+
<a name="ts-prefer-function-type-failing-examples"></a>
33+
## Failing examples
34+
35+
The following patterns are considered problems:
36+
37+
````ts
38+
/**
39+
* @param {{
40+
* (arg: string): void;
41+
* }} someName
42+
*/
43+
// Message: Call signature found; function type preferred.
44+
45+
/**
46+
* @param {{
47+
* (arg: string): void;
48+
* }} someName
49+
*/
50+
// "jsdoc/ts-prefer-function-type": ["error"|"warn", {"enableFixer":false}]
51+
// Message: Call signature found; function type preferred.
52+
53+
/**
54+
* @param {(string | {
55+
* (arg: string): void;
56+
* })} someName
57+
*/
58+
// Message: Call signature found; function type preferred.
59+
````
60+
61+
62+
63+
<a name="user-content-ts-prefer-function-type-passing-examples"></a>
64+
<a name="ts-prefer-function-type-passing-examples"></a>
65+
## Passing examples
66+
67+
The following patterns are not considered problems:
68+
69+
````ts
70+
/**
71+
* @param {() => number} someName
72+
*/
73+
74+
/**
75+
* @param {{
76+
* (arg: string): void;
77+
* abc: number;
78+
* }} someName
79+
*/
80+
81+
/**
82+
* @param {{
83+
* (data: string): number;
84+
* (id: number): string;
85+
* }} someName
86+
*/
87+
88+
/**
89+
* @param {BadType<} someName
90+
*/
91+
````
92+

src/index-cjs.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import sortTags from './rules/sortTags.js';
6767
import tagLines from './rules/tagLines.js';
6868
import textEscaping from './rules/textEscaping.js';
6969
import tsMethodSignatureStyle from './rules/tsMethodSignatureStyle.js';
70+
import tsPreferFunctionType from './rules/tsPreferFunctionType.js';
7071
import typeFormatting from './rules/typeFormatting.js';
7172
import validTypes from './rules/validTypes.js';
7273

@@ -253,6 +254,7 @@ index.rules = {
253254
'tag-lines': tagLines,
254255
'text-escaping': textEscaping,
255256
'ts-method-signature-style': tsMethodSignatureStyle,
257+
'ts-prefer-function-type': tsPreferFunctionType,
256258
'type-formatting': typeFormatting,
257259
'valid-types': validTypes,
258260
};
@@ -344,6 +346,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => {
344346
'jsdoc/tag-lines': warnOrError,
345347
'jsdoc/text-escaping': 'off',
346348
'jsdoc/ts-method-signature-style': 'off',
349+
'jsdoc/ts-prefer-function-type': 'off',
347350
'jsdoc/type-formatting': 'off',
348351
'jsdoc/valid-types': warnOrError,
349352
},

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import sortTags from './rules/sortTags.js';
7373
import tagLines from './rules/tagLines.js';
7474
import textEscaping from './rules/textEscaping.js';
7575
import tsMethodSignatureStyle from './rules/tsMethodSignatureStyle.js';
76+
import tsPreferFunctionType from './rules/tsPreferFunctionType.js';
7677
import typeFormatting from './rules/typeFormatting.js';
7778
import validTypes from './rules/validTypes.js';
7879

@@ -259,6 +260,7 @@ index.rules = {
259260
'tag-lines': tagLines,
260261
'text-escaping': textEscaping,
261262
'ts-method-signature-style': tsMethodSignatureStyle,
263+
'ts-prefer-function-type': tsPreferFunctionType,
262264
'type-formatting': typeFormatting,
263265
'valid-types': validTypes,
264266
};
@@ -350,6 +352,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => {
350352
'jsdoc/tag-lines': warnOrError,
351353
'jsdoc/text-escaping': 'off',
352354
'jsdoc/ts-method-signature-style': 'off',
355+
'jsdoc/ts-prefer-function-type': 'off',
353356
'jsdoc/type-formatting': 'off',
354357
'jsdoc/valid-types': warnOrError,
355358
},

src/rules.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2952,6 +2952,18 @@ export interface Rules {
29522952
}
29532953
];
29542954

2955+
/** */
2956+
"jsdoc/ts-prefer-function-type":
2957+
| []
2958+
| [
2959+
{
2960+
/**
2961+
* Whether to enable the fixer or not
2962+
*/
2963+
enableFixer?: boolean;
2964+
}
2965+
];
2966+
29552967
/** Formats JSDoc type values. */
29562968
"jsdoc/type-formatting":
29572969
| []

src/rules/tsPreferFunctionType.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
utils,
15+
}) => {
16+
const {
17+
enableFixer = true,
18+
} = context.options[0] || {};
19+
20+
/**
21+
* @param {import('@es-joy/jsdoccomment').JsdocTagWithInline} tag
22+
*/
23+
const checkType = (tag) => {
24+
const potentialType = tag.type;
25+
26+
/** @type {import('jsdoc-type-pratt-parser').RootResult} */
27+
let parsedType;
28+
try {
29+
parsedType = parseType(
30+
/** @type {string} */ (potentialType), 'typescript',
31+
);
32+
} catch {
33+
return;
34+
}
35+
36+
traverse(parsedType, (nde, parentNode) => {
37+
// @ts-expect-error Adding our own property for use below
38+
nde.parentNode = parentNode;
39+
});
40+
41+
traverse(parsedType, (nde, parentNode, property, index) => {
42+
switch (nde.type) {
43+
case 'JsdocTypeCallSignature': {
44+
const object = /** @type {import('jsdoc-type-pratt-parser').ObjectResult} */ (
45+
parentNode
46+
);
47+
if (typeof index === 'number' && object.elements.length === 1) {
48+
utils.reportJSDoc(
49+
'Call signature found; function type preferred.',
50+
tag,
51+
enableFixer ? () => {
52+
const func = /** @type {import('jsdoc-type-pratt-parser').FunctionResult} */ ({
53+
arrow: true,
54+
constructor: false,
55+
meta: /** @type {Required<import('jsdoc-type-pratt-parser').MethodSignatureResult['meta']>} */ (
56+
nde.meta
57+
),
58+
parameters: nde.parameters,
59+
parenthesis: true,
60+
returnType: nde.returnType,
61+
type: 'JsdocTypeFunction',
62+
typeParameters: nde.typeParameters,
63+
});
64+
65+
if (property && 'parentNode' in object && object.parentNode) {
66+
if (typeof object.parentNode === 'object' &&
67+
'elements' in object.parentNode &&
68+
Array.isArray(object.parentNode.elements)
69+
) {
70+
const idx = object.parentNode.elements.indexOf(object);
71+
object.parentNode.elements[idx] = func;
72+
/* c8 ignore next 6 -- Guard */
73+
} else {
74+
throw new Error(
75+
// @ts-expect-error Ok
76+
`Rule currently unable to handle type ${object.parentNode.type}`,
77+
);
78+
}
79+
} else {
80+
parsedType = func;
81+
}
82+
83+
rewireByParsedType(jsdoc, tag, parsedType, indent);
84+
} : null,
85+
);
86+
}
87+
88+
break;
89+
}
90+
}
91+
});
92+
};
93+
94+
const tags = utils.filterTags(({
95+
tag,
96+
}) => {
97+
return Boolean(tag !== 'import' && utils.tagMightHaveTypePosition(tag));
98+
});
99+
100+
for (const tag of tags) {
101+
if (tag.type) {
102+
checkType(tag);
103+
}
104+
}
105+
}, {
106+
iterateAllJsdocs: true,
107+
meta: {
108+
docs: {
109+
description: '',
110+
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/ts-prefer-function-type.md#repos-sticky-header',
111+
},
112+
fixable: 'code',
113+
schema: [
114+
{
115+
additionalProperties: false,
116+
properties: {
117+
enableFixer: {
118+
description: 'Whether to enable the fixer or not',
119+
type: 'boolean',
120+
},
121+
},
122+
type: 'object',
123+
},
124+
],
125+
type: 'suggestion',
126+
},
127+
});

0 commit comments

Comments
 (0)