Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions common/build/eslint-plugin-fluid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"devDependencies": {
"@fluid-tools/markdown-magic": "file:../../../tools/markdown-magic",
"@fluidframework/build-common": "^2.0.3",
"@types/eslint": "^9.6.1",
"cross-env": "^7.0.3",
"eslint": "~9.37.0",
"eslint8": "npm:eslint@~8.57.1",
Expand Down
11 changes: 11 additions & 0 deletions common/build/eslint-plugin-fluid/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,61 @@
* Licensed under the MIT License.
*/

//@ts-check
/**
* @typedef {import("eslint").Rule.RuleModule} RuleModule
* @typedef {import('@microsoft/tsdoc').DocNode} DocNode
* @typedef {import('@microsoft/tsdoc').DocPlainText} DocPlainText
*/

const { DocNodeKind, TSDocParser } = require("@microsoft/tsdoc");

const parser = new TSDocParser();

/**
* Checks if a comment text starts with a hyphen.
* @param {DocPlainText} plainTextNode - The plain text node to check.
*/
function doesTextNodeStartWithHyphen(plainTextNode) {
return plainTextNode.text.trimStart().startsWith("-");
}

/**
* Checks if a comment body starts with a hyphen.
* @param { DocNode } commentBodyNode - The doc node representing the body of the comment.
*/
function doesCommentBodyStartWithHyphen(commentBodyNode) {
// Walk down first node of the tree until we find a leaf.
// If it's plain text, and starts with a hyphen, return true.
// Otherwise, return false.
if (commentBodyNode.kind === DocNodeKind.PlainText) {
return doesTextNodeStartWithHyphen(/** @type {DocPlainText} */ (commentBodyNode));
}

const childNodes = commentBodyNode.getChildNodes();
if (childNodes.length === 0) {
return false;
}

return doesCommentBodyStartWithHyphen(childNodes[0]);
}

/**
* JSDoc/TSDoc tags do not require a hyphen after them.
* @type {RuleModule}
*/
module.exports = {
const rule = {
meta: {
type: "problem",
docs: {
description: "Disallow hyphen character immediately following JSDoc tag",
description: "Disallow hyphen character immediately following JSDoc/TSDoc block tag",
category: "Best Practices",
recommended: false,
},
messages: {
hyphenAfterTag:
"JSDoc/TSDoc block tags should not be followed by a hyphen character ('-').",
"JSDoc/TSDoc block tags must not be followed by a hyphen character (`-`).",
},
fixable: "code",
schema: [],
},

Expand All @@ -32,33 +71,62 @@ module.exports = {
.filter((comment) => comment.type === "Block" && comment.value.startsWith("*"));

for (const comment of comments) {
// +2 for the leading "/*", which is omitted by `comment.value`, but included in `comment.range`.
const commentStartIndex = comment.range[0] + 2;

// Find any JSDoc/TSDoc tags followed by a hyphen
const matches = comment.value.matchAll(/(@[a-zA-Z0-9]+)\s*?-\s*?(.*)/g);
for (const match of matches) {
const [fullMatch, tag, body] = match;

const startIndex = commentStartIndex + match.index;
const endIndex = startIndex + fullMatch.length;

context.report({
loc: {
start: sourceCode.getLocFromIndex(startIndex),
end: sourceCode.getLocFromIndex(endIndex),
},
messageId: "hyphenAfterTag",
fix(fixer) {
return fixer.replaceTextRange(
[startIndex, endIndex],
`${tag} ${body.trimStart()}`,
);
},
});
if (comment.range === undefined) {
continue;
}

const commentStartIndex = comment.range[0];

// TSDoc parser requires the surrounding "/**" and "*/", but eslint strips those off in `comment.value`.
const parserContext = parser.parseString(`/**${comment.value}*/`);
const parsedComment = parserContext.docComment;

const blocksToCheck = [
...parsedComment.customBlocks,
...parsedComment.seeBlocks,
];
if (parsedComment.remarksBlock) {
blocksToCheck.push(parsedComment.remarksBlock);
}
if (parsedComment.privateRemarks) {
blocksToCheck.push(parsedComment.privateRemarks);
}
if (parsedComment.deprecatedBlock) {
blocksToCheck.push(parsedComment.deprecatedBlock);
}
if (parsedComment.returnsBlock) {
blocksToCheck.push(parsedComment.returnsBlock);
}

// Note: the TSDoc format makes it difficult to extract the range information for the block content specifically.
// Instead, we just report the range for the tag itself.
for (const block of blocksToCheck) {
if (doesCommentBodyStartWithHyphen(block.content)) {
const tagTextRange = block.blockTag
.getTokenSequence()
.getContainingTextRange();
const tagTextRangeStart = tagTextRange.pos - 1; // Include the `@`
const tagTextRangeEnd = tagTextRange.end;
const startIndex = sourceCode.getLocFromIndex(
commentStartIndex + tagTextRangeStart,
);
const endIndex = sourceCode.getLocFromIndex(
commentStartIndex + tagTextRangeEnd,
);

context.report({
loc: {
start: startIndex,
end: endIndex,
},
messageId: "hyphenAfterTag",
});
}
}
}
},
};
},
};

module.exports = rule;
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe(`Do not allow \`-\` following JSDoc/TSDoc tags (eslint ${eslintVersion}
}

const expectedErrorMessage =
"JSDoc/TSDoc block tags should not be followed by a hyphen character ('-').";
"JSDoc/TSDoc block tags must not be followed by a hyphen character (`-`).";

it("Should report errors JSDoc/TSDoc tags followed by a hyphen", async function () {
const result = await lintFile("test.ts");
Expand All @@ -39,32 +39,20 @@ describe(`Do not allow \`-\` following JSDoc/TSDoc tags (eslint ${eslintVersion}
assert.strictEqual(error1.message, expectedErrorMessage);
assert.strictEqual(error1.line, 8);
assert.strictEqual(error1.column, 4); // 1-based, inclusive
assert.strictEqual(error1.endColumn, 37); // 1-based, exclusive
assert(error1.fix !== undefined);
assert.strictEqual(error1.fix.text, "@remarks Here are some remarks.");
assert.deepEqual(error1.fix.range, [226, 259]); // 0-based global character index in the file. The start is inclusive, and the end is exclusive.
assert.strictEqual(error1.endColumn, 13); // 1-based, exclusive

// Error 2
const error2 = result.messages[1];
assert.strictEqual(error2.message, expectedErrorMessage);
assert.strictEqual(error2.line, 9);
assert.strictEqual(error2.column, 4); // 1-based, inclusive
assert.strictEqual(error2.endColumn, 65); // 1-based, exclusive
assert(error2.fix !== undefined);
assert.strictEqual(
error2.fix.text,
"@deprecated This function is deprecated, use something else.",
);
assert.deepEqual(error2.fix.range, [263, 324]); // 0-based global character index in the file. The start is inclusive, and the end is exclusive.
assert.strictEqual(error2.endColumn, 16); // 1-based, exclusive

// Error 3
const error3 = result.messages[2];
assert.strictEqual(error3.message, expectedErrorMessage);
assert.strictEqual(error3.line, 10);
assert.strictEqual(error3.column, 4); // 1-based, inclusive
assert.strictEqual(error3.endColumn, 39); // 1-based, exclusive
assert(error3.fix !== undefined);
assert.strictEqual(error3.fix.text, "@returns The concatenated string.");
assert.deepEqual(error3.fix.range, [328, 363]); // 0-based global character index in the file. The start is inclusive, and the end is exclusive.
assert.strictEqual(error3.endColumn, 13); // 1-based, exclusive
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/**
* I am a test function with pretty standard docs, but all of my tags have hyphens after them :(
* @remarks - Here are some remarks.
* @deprecated- This function is deprecated, use something else.
* @deprecated - This function is deprecated, use something else.
* @returns - The concatenated string.
*/
function invalid<T>(param1: string, param2: T): string {
Expand All @@ -15,11 +15,19 @@ function invalid<T>(param1: string, param2: T): string {

/**
* I am a test function with pretty standard docs, and none of my tags have hyphens after them :)
* \@tag - Escaped tags should be valid since the @ is escaped.
* `@tag - in a code block` - This should also be valid.
* I also have a {@link @foo/bar | link} - This should not trigger the rule.
* @remarks Here are some remarks.
* @deprecated This function is deprecated, use something else.
* @returns The concatenated string.
* @param param1 - I am a param comment. Since my hyphen follows the param name, this is valid.
* @typeParam T - I am a type param comment. I am also valid.
* @example
* An example that should be valid:
* ```
* @foo - Bar (this should be ignored by the rule, since it occurs within a fenced code block and therefore cannot be an actual tag)
* ```
*/
function valid<T>(param1: string, param2: T): string {
return `${param1} - ${param2}`;
Expand Down