Skip to content

Commit 0e9a1b9

Browse files
authored
feat(eslint-plugin-fluid): Add a rule preventing the use of - immediately following a JSDoc/TSDoc tag (#25556)
Following a TSDoc block tag with a `-` is a commonly made mistake. Block tags do not want a `-` between the tag and the content. This PR adds a rule to help detect and prevent this anti-pattern.
1 parent d895f88 commit 0e9a1b9

File tree

7 files changed

+164
-3
lines changed

7 files changed

+164
-3
lines changed

common/build/eslint-plugin-fluid/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# @fluidframework/eslint-plugin-fluid Changelog
22

3+
## [0.3.0](https://github.com/microsoft/FluidFramework/releases/tag/eslint-plugin-fluid_v0.2.0)
4+
5+
New rules added:
6+
7+
- `@fluid-internal/fluid/no-hyphen-after-jsdoc-tag`: Forbids following a JSDoc/TSDoc comment tag with a `-`.
8+
- Such syntax is commonly used by mistake, due to the fact that `TSDoc` requires a hyphen after the parameter name of a `@param` comment. But no tags want a hyphen between the tag name and the body.
9+
310
## [0.2.0](https://github.com/microsoft/FluidFramework/releases/tag/eslint-plugin-fluid_v0.2.0)
411

512
New rules added:

common/build/eslint-plugin-fluid/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
*/
1313
module.exports = {
1414
rules: {
15+
/**
16+
* Disallow `-` following JSDoc/TSDoc tags.
17+
* Full name: "@fluid-internal/fluid/no-hyphen-after-jsdoc-tag"
18+
*/
19+
"no-hyphen-after-jsdoc-tag": require("./src/rules/no-hyphen-after-jsdoc-tag"),
20+
1521
/**
1622
* Disallow file path links in JSDoc/TSDoc comments.
1723
* Full name: "@fluid-internal/fluid/no-file-path-links-in-jsdoc"

common/build/eslint-plugin-fluid/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@fluid-internal/eslint-plugin-fluid",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "Custom ESLint rules for the Fluid Framework",
55
"homepage": "https://fluidframework.com",
66
"repository": {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*!
2+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
/**
7+
* JSDoc/TSDoc tags do not require a hyphen after them.
8+
*/
9+
module.exports = {
10+
meta: {
11+
type: "problem",
12+
docs: {
13+
description: "Disallow hyphen character immediately following JSDoc tag",
14+
category: "Best Practices",
15+
recommended: false,
16+
},
17+
messages: {
18+
hyphenAfterTag:
19+
"JSDoc/TSDoc block tags should not be followed by a hyphen character ('-').",
20+
},
21+
fixable: "code",
22+
schema: [],
23+
},
24+
25+
create(context) {
26+
return {
27+
Program() {
28+
const sourceCode = context.getSourceCode();
29+
const comments = sourceCode
30+
.getAllComments()
31+
// Filter to only JSDoc/TSDoc style block comments
32+
.filter((comment) => comment.type === "Block" && comment.value.startsWith("*"));
33+
34+
for (const comment of comments) {
35+
// Find any JSDoc/TSDoc tags followed by a hyphen
36+
const matches = comment.value.matchAll(/(@[a-zA-Z0-9]+)\s*?-(.*)/g);
37+
for (const match of matches) {
38+
const [fullMatch, tag, body] = match;
39+
40+
const startIndex = comment.range[0] + match.index;
41+
const endIndex = startIndex + fullMatch.length;
42+
43+
context.report({
44+
loc: {
45+
start: sourceCode.getLocFromIndex(startIndex),
46+
end: sourceCode.getLocFromIndex(endIndex),
47+
},
48+
messageId: "hyphenAfterTag",
49+
fix(fixer) {
50+
return fixer.replaceTextRange(
51+
[startIndex, endIndex],
52+
`${tag} ${body.trimStart()}`,
53+
);
54+
},
55+
});
56+
}
57+
}
58+
},
59+
};
60+
},
61+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*!
2+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
const assert = require("assert");
7+
const path = require("path");
8+
const { ESLint } = require("eslint");
9+
10+
describe("Do not allow `-` following JSDoc/TSDoc tags", function () {
11+
/**
12+
*
13+
* @param {string} file - Path to the file being linted. Relative to the `example/no-hyphen-after-jsdoc-tag` folder.
14+
* @returns
15+
*/
16+
async function lintFile(file) {
17+
const eslint = new ESLint({
18+
useEslintrc: false,
19+
overrideConfig: {
20+
rules: {
21+
"no-hyphen-after-jsdoc-tag": "error",
22+
},
23+
parser: "@typescript-eslint/parser",
24+
parserOptions: {
25+
project: path.join(__dirname, "../example/tsconfig.json"),
26+
},
27+
},
28+
rulePaths: [path.join(__dirname, "../../rules")],
29+
});
30+
const fileToLint = path.join(__dirname, "../example/no-hyphen-after-jsdoc-tag", file);
31+
const results = await eslint.lintFiles([fileToLint]);
32+
assert.equal(results.length, 1, "Expected a single result for linting a single file.");
33+
return results[0];
34+
}
35+
36+
const expectedErrorMessage =
37+
"JSDoc/TSDoc block tags should not be followed by a hyphen character ('-').";
38+
39+
it("Should report errors JSDoc/TSDoc tags followed by a hyphen", async function () {
40+
const result = await lintFile("test.ts");
41+
assert.strictEqual(result.errorCount, 3);
42+
43+
// Error 1
44+
assert.strictEqual(result.messages[0].message, expectedErrorMessage);
45+
assert.strictEqual(result.messages[0].line, 8);
46+
assert.strictEqual(result.messages[0].fix?.text, "@remarks Here are some remarks.");
47+
48+
// Error 2
49+
assert.strictEqual(result.messages[1].message, expectedErrorMessage);
50+
assert.strictEqual(result.messages[1].line, 9);
51+
assert.strictEqual(
52+
result.messages[1].fix?.text,
53+
"@deprecated This function is deprecated, use something else.",
54+
);
55+
56+
// Error 3
57+
assert.strictEqual(result.messages[2].message, expectedErrorMessage);
58+
assert.strictEqual(result.messages[2].line, 10);
59+
assert.strictEqual(result.messages[2].fix?.text, "@returns The concatenated string.");
60+
});
61+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*!
2+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
/**
7+
* I am a test function with pretty standard docs, but all of my tags have hyphens after them ☹️.
8+
* @remarks - Here are some remarks.
9+
* @deprecated- This function is deprecated, use something else.
10+
* @returns - The concatenated string.
11+
*/
12+
function invalid<T>(param1: string, param2: T): string {
13+
return `${param1} - ${param2}`;
14+
}
15+
16+
/**
17+
* I am a test function with pretty standard docs, and none of my tags have hyphens after them 🙂.
18+
* @remarks Here are some remarks.
19+
* @deprecated This function is deprecated, use something else.
20+
* @returns The concatenated string.
21+
* @param param1 - I am a param comment. Since my hyphen follows the param name, this is valid.
22+
* @typeParam T - I am a type param comment. I am also valid.
23+
*/
24+
function valid<T>(param1: string, param2: T): string {
25+
return `${param1} - ${param2}`;
26+
}

common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/indexedRecordOfStrings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,11 @@ if (key in datastore) {
143143
}
144144
datastore[key][key] = {}; // ok: Accessing nested property key of datastore[key] is allowed because it is assigned in the else case
145145

146-
if (indexedRecordOfStrings.a !== void(0)) {
146+
if (indexedRecordOfStrings.a !== void 0) {
147147
indexedRecordOfStrings.a.length; // ok: Within a presence check, 'a' is guaranteed to be defined
148148
}
149149

150-
if (indexedRecordOfStrings.a !== void(1)) {
150+
if (indexedRecordOfStrings.a !== void 1) {
151151
indexedRecordOfStrings.a.length; // ok: Within a presence check, 'a' is guaranteed to be defined
152152
}
153153

0 commit comments

Comments
 (0)