Skip to content

Commit

Permalink
Upgrade "boolean-trivia" lint to new "argument-trivia" lint that uses…
Browse files Browse the repository at this point in the history
… type info, has quick fixes, etc. (#53002)
  • Loading branch information
jakebailey authored Mar 23, 2023
1 parent 3a3146e commit ac55b29
Show file tree
Hide file tree
Showing 89 changed files with 615 additions and 528 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"allowDeclarations": true
}],
"local/no-double-space": "error",
"local/boolean-trivia": "error",
"local/argument-trivia": "error",
"local/no-in-operator": "error",
"local/simple-indent": "error",
"local/debug-assert": "error",
Expand Down
203 changes: 203 additions & 0 deletions scripts/eslint/rules/argument-trivia.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
const { AST_NODE_TYPES, TSESTree, ESLintUtils } = require("@typescript-eslint/utils");
const { createRule } = require("./utils.cjs");
const ts = require("typescript");

const unset = Symbol();
/**
* @template T
* @param {() => T} fn
* @returns {() => T}
*/
function memoize(fn) {
/** @type {T | unset} */
let value = unset;
return () => {
if (value === unset) {
value = fn();
}
return value;
};
}


module.exports = createRule({
name: "argument-trivia",
meta: {
docs: {
description: ``,
recommended: "error",
},
messages: {
argumentTriviaArgumentError: `Tag argument with parameter name`,
argumentTriviaArgumentSpaceError: `There should be 1 space between an argument and its comment`,
argumentTriviaArgumentNameError: `Argument name "{{ got }}" does not match expected name "{{ want }}"`,
},
schema: [],
type: "problem",
fixable: "code",
},
defaultOptions: [],

create(context) {
const sourceCode = context.getSourceCode();
const sourceCodeText = sourceCode.getText();

/** @type {(name: string) => boolean} */
const isSetOrAssert = (name) => name.startsWith("set") || name.startsWith("assert");
/** @type {(node: TSESTree.Node) => boolean} */
const isTrivia = (node) => {
if (node.type === AST_NODE_TYPES.Identifier) {
return node.name === "undefined";
}

if (node.type === AST_NODE_TYPES.Literal) {
// eslint-disable-next-line no-null/no-null
return node.value === null || node.value === true || node.value === false;
}

return false;
};

/** @type {(node: TSESTree.CallExpression | TSESTree.NewExpression) => boolean} */
const shouldIgnoreCalledExpression = (node) => {
if (node.callee && node.callee.type === AST_NODE_TYPES.MemberExpression) {
const methodName = node.callee.property.type === AST_NODE_TYPES.Identifier
? node.callee.property.name
: "";

if (isSetOrAssert(methodName)) {
return true;
}

switch (methodName) {
case "apply":
case "call":
case "equal":
case "stringify":
case "push":
return true;
}

return false;
}

if (node.callee && node.callee.type === AST_NODE_TYPES.Identifier) {
const functionName = node.callee.name;

if (isSetOrAssert(functionName)) {
return true;
}

switch (functionName) {
case "contains":
return true;
}

return false;
}

return false;
};


/** @type {(node: TSESTree.Node, i: number, getSignature: () => ts.Signature | undefined) => void} */
const checkArg = (node, i, getSignature) => {
if (!isTrivia(node)) {
return;
}

const getExpectedName = memoize(() => {
const signature = getSignature();
if (signature) {
const expectedName = signature.parameters[i]?.escapedName;
if (expectedName) {
const name = ts.unescapeLeadingUnderscores(expectedName);
// If a parameter is unused, we prepend an underscore. Ignore this
// so that we can switch between used and unused without modifying code,
// requiring that arugments are tagged with the non-underscored name.
return name.startsWith("_") ? name.slice(1) : name;
}
}
return undefined;
});

const comments = sourceCode.getCommentsBefore(node);
/** @type {TSESTree.Comment | undefined} */
const comment = comments[comments.length - 1];

if (!comment || comment.type !== "Block") {
const expectedName = getExpectedName();
if (expectedName) {
context.report({
messageId: "argumentTriviaArgumentError",
node,
fix: (fixer) => {
return fixer.insertTextBefore(node, `/*${expectedName}*/ `);
}
});
}
else {
context.report({ messageId: "argumentTriviaArgumentError", node });
}
return;
}

const argRangeStart = node.range[0];
const commentRangeEnd = comment.range[1];
const expectedName = getExpectedName();
if (expectedName) {
const got = comment.value;
if (got !== expectedName) {
context.report({
messageId: "argumentTriviaArgumentNameError",
data: { got, want: expectedName },
node: comment,
fix: (fixer) => {
return fixer.replaceText(comment, `/*${expectedName}*/`);
},
});
return;
}
}

const hasNewLine = sourceCodeText.slice(commentRangeEnd, argRangeStart).indexOf("\n") >= 0;
if (argRangeStart !== commentRangeEnd + 1 && !hasNewLine) {
// TODO(jakebailey): range should be whitespace
context.report({
messageId: "argumentTriviaArgumentSpaceError",
node,
fix: (fixer) => {
return fixer.replaceTextRange([commentRangeEnd, argRangeStart], " ");
}
});
}
};

/** @type {(node: TSESTree.CallExpression | TSESTree.NewExpression) => void} */
const checkArgumentTrivia = (node) => {
if (shouldIgnoreCalledExpression(node)) {
return;
}

const getSignature = memoize(() => {
if (context.parserServices?.hasFullTypeInformation) {
const parserServices = ESLintUtils.getParserServices(context);
const checker = parserServices.program.getTypeChecker();
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
return checker.getResolvedSignature(tsNode);
}
return undefined;
});

for (let i = 0; i < node.arguments.length; i++) {
const arg = node.arguments[i];
checkArg(arg, i, getSignature);
}
};

return {
CallExpression: checkArgumentTrivia,
NewExpression: checkArgumentTrivia,
};
},
});
110 changes: 0 additions & 110 deletions scripts/eslint/rules/boolean-trivia.cjs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { RuleTester } = require("./support/RuleTester.cjs");
const rule = require("../rules/boolean-trivia.cjs");
const rule = require("../rules/argument-trivia.cjs");

const ruleTester = new RuleTester({
parserOptions: {
Expand All @@ -8,7 +8,7 @@ const ruleTester = new RuleTester({
parser: require.resolve("@typescript-eslint/parser"),
});

ruleTester.run("boolean-trivia", rule, {
ruleTester.run("argument-trivia", rule, {
valid: [
{
code: `
Expand Down Expand Up @@ -48,6 +48,12 @@ const fn = (prop: boolean) => {};
fn.apply(null, true);
`,
},
{
code: `
const fn = (prop: boolean) => {};
fn(/* first comment */ /* second comment */ false);
`,
},
],

invalid: [
Expand All @@ -56,28 +62,25 @@ fn.apply(null, true);
const fn = (prop: null) => {};
fn(null);
`,
errors: [{ messageId: "booleanTriviaArgumentError" }],
errors: [{ messageId: "argumentTriviaArgumentError" }],
},
{
code: `
const fn = (prop: boolean) => {};
fn(false);
`,
errors: [{ messageId: "booleanTriviaArgumentError" }],
errors: [{ messageId: "argumentTriviaArgumentError" }],
},
{
code: `
const fn = (prop: boolean) => {};
fn(/* boolean arg */false);
`,
errors: [{ messageId: "booleanTriviaArgumentSpaceError" }],
},
{
code: `
errors: [{ messageId: "argumentTriviaArgumentSpaceError" }],
output:`
const fn = (prop: boolean) => {};
fn(/* first comment */ /* second comment */ false);
`,
errors: [{ messageId: "booleanTriviaArgumentError" }],
fn(/* boolean arg */ false);
`
},
],
});
Loading

0 comments on commit ac55b29

Please sign in to comment.