Skip to content

added scripts to generate a new rule with the command line #130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 21, 2024
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
12 changes: 9 additions & 3 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ module.exports = {
],
plugins: ["header"],
env: {
node: true
node: true,
es6: true
},
parserOptions: {
ecmaVersion: 2021, // Allows the latest ECMAScript features
sourceType: "module" // Ensures `import` and `export` syntax are valid
},
overrides: [
{
Expand All @@ -23,7 +28,8 @@ module.exports = {
}
],
rules: {
"header/header": [2, "line", [" Copyright (c) Microsoft Corporation.", " Licensed under the MIT License."], 2]
"header/header": [2, "line", [" Copyright (c) Microsoft Corporation.", " Licensed under the MIT License."], 2],
"no-console": "warn" // Add this to warn about console statements
},
ignorePatterns: ["node_modules", "dist/"]
ignorePatterns: ["node_modules", "dist/", "scripts"]
};
8 changes: 3 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any addi

- Install [Node.js](https://nodejs.org/en/), with [nvm](https://github.com/nvm-sh/nvm). Please use Node version 16 and npm v 8.

- Install [jscodeshift](https://github.com/facebook/jscodeshift) globally.

- **Internal collaborators:** Please send Aubrey Quinn your email address. After getting `Write` access, you can create branches and directly submit pull requests against this repo. Make and submit changes following the [pull request submission workflow](#pull-requests)
- **External collaborators:** [Fork the repo and clone your fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo)

Expand Down Expand Up @@ -51,11 +53,7 @@ or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any addi

If you want to create a new ESLint rule:

1. create a rule file under `lib/rules`
2. create a doc file under `docs`
3. create a test file under `tests`
4. add your rule to `lib/rules/index.ts`
5. add your rule to `index.ts`
1. Please run the [create-rule](./scripts/create-rule.md) script.

## Pull requests

Expand Down
898 changes: 770 additions & 128 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"fix:md": "npm run lint:docs -- --fix",
"test:new": "jest",
"prepare": "husky",
"test:coverage": "jest --coverage"
"test:coverage": "jest --coverage",
"create": "node ./scripts/create-rule"
},
"dependencies": {
"eslint-plugin-header": "^3.1.1",
Expand All @@ -72,12 +73,14 @@
"eslint-plugin-node": "^11.1.0",
"husky": "^9.1.6",
"jest": "^29.7.0",
"jscodeshift": "^17.0.0",
"markdownlint": "^0.28.1",
"markdownlint-cli": "^0.33.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.4",
"ts-jest": "^29.2.5",
"typescript": "^5.6.2"
"typescript": "^5.6.2",
"yargs": "^17.7.2"
},
"engines": {
"node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
Expand Down
77 changes: 77 additions & 0 deletions scripts/addRuleToExportIndex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import fs from "fs";
import { kebabToCamelCase } from "./utils/kebabToKamelCase";

function transformer(file, api, options) {
const j = api.jscodeshift;
const { ruleName, exportIndexFilePath } = options;

// Read the export index file content
const exportIndexSource = j(fs.readFileSync(exportIndexFilePath, "utf8"));

// Convert the rule name to camelCase for the export statement
const exportRuleName = kebabToCamelCase(ruleName);

// Validate ruleName and exportRuleName
if (!ruleName || !exportRuleName) {
throw new Error(`Invalid rule name or export rule name: ${ruleName}, ${exportRuleName}`);
}

// Create the new export specifier
const specifier = j.exportSpecifier.from({
exported: j.identifier(exportRuleName),
local: j.identifier("default")
});

// Create the new export statement
const newExportStatement = j.exportNamedDeclaration(null, [specifier], j.stringLiteral(`./${ruleName}`));

// Find all export statements
const exportStatements = exportIndexSource.find(j.ExportNamedDeclaration);

if (exportStatements.size() === 0) {
// No export statements found, so insert at the beginning of the file
exportIndexSource.get().node.program.body.unshift(newExportStatement);
} else {
// Insert the new export statement after the last one
const lastExportStatement = exportStatements.paths()[exportStatements.size() - 1];
j(lastExportStatement).insertAfter(newExportStatement);

// Re-query the export statements after the insertion
const updatedExportStatements = exportIndexSource.find(j.ExportNamedDeclaration);

// Manually sort the export statements alphabetically
const sortedExports = updatedExportStatements
.nodes()
.map(node => {
if (node.specifiers && node.specifiers[0] && node.specifiers[0].exported) {
return node;
}
return null; // Ignore nodes without valid specifiers
})
.filter(node => node !== null) // Remove nulls
.sort((a, b) => {
const aName = a.specifiers[0].exported.name;
const bName = b.specifiers[0].exported.name;
return aName.localeCompare(bName);
});

// Remove all the original export statements
exportIndexSource.find(j.ExportNamedDeclaration).remove();

// Now insert the sorted export statements back into the AST
const body = exportIndexSource.get().node.program.body;
sortedExports.forEach(exportNode => {
body.push(exportNode); // Insert each export statement at the end of the body
});
}

// Write the modified index file back to the filesystem
fs.writeFileSync(exportIndexFilePath, exportIndexSource.toSource({ quote: "double" }), "utf8");

// Return the original file source (this is for the main file passed in)
return file.source;
}
module.exports = transformer;
56 changes: 56 additions & 0 deletions scripts/addRuleToIndex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { kebabToCamelCase } from "./utils/kebabToKamelCase";

// Sort function to keep rules and config sorted alphabetically
const nameSort = (a, b) => {
const aName = a.key.type === "Literal" ? a.key.value : a.key.name;
const bName = a.key.type === "Literal" ? b.key.value : b.key.name;
if (aName < bName) return -1;
if (aName > bName) return 1;
return 0;
};

const transformer = (file, api, options) => {
const j = api.jscodeshift;
const root = j(file.source);
const { ruleName } = options; // No need for rulePath in this case

let changesMade = 0;

// Step 1: Add rule to the `rules` object (without parentheses)
root.find(j.Property, { key: { name: "rules" } })
.at(0)
.forEach(path => {
const properties = path.value.value.properties;
properties.unshift(
j.property("init", j.literal(ruleName), j.memberExpression(j.identifier("rules"), j.identifier(kebabToCamelCase(ruleName))))
);
properties.sort(nameSort);
changesMade += 1;
});

// Step 2: Find and modify `configs.recommended.rules`
root.find(j.Property, { key: { name: "configs" } }).forEach(configPath => {
const recommendedConfig = configPath.value.value.properties.find(prop => prop.key.name === "recommended");

if (recommendedConfig) {
const recommendedRules = recommendedConfig.value.properties.find(prop => prop.key.name === "rules");

if (recommendedRules) {
const rulesProps = recommendedRules.value.properties;
rulesProps.unshift(j.property("init", j.literal(`@microsoft/fluentui-jsx-a11y/${ruleName}`), j.literal("error")));
rulesProps.sort(nameSort);
changesMade += 1;
}
}
});

if (changesMade === 0) {
return null;
}

return root.toSource({ quote: "double", trailingComma: false });
};
module.exports = transformer;
22 changes: 22 additions & 0 deletions scripts/boilerplate/doc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

const docBoilerplateGenerator = (name, description) => `# ${description} (@microsoft/fluentui-jsx-a11y/${name})

Write a useful explanation here!

## Rule details

Write more details here!

\`\`\`jsx
<div />
\`\`\`

\`\`\`jsx
<input />
\`\`\`

## Further Reading
`;
module.exports = docBoilerplateGenerator;
45 changes: 45 additions & 0 deletions scripts/boilerplate/rule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

const ruleBoilerplate = (name, description) => `// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

// RuleCreator requires a URL or documentation link, but it can be a placeholder
const createRule = ESLintUtils.RuleCreator(name => "https://example.com/rule/${name}");

const rule = createRule({
name: "${name}",
meta: {
type: "suggestion", // could be "problem", "suggestion", or "layout"
docs: {
description: "${description}",
recommended: "error" // could also be "warn"
},
messages: {
errorMessage: "" // describe the issue
},
schema: [] // no options for this rule
},
defaultOptions: [], // no options needed
create(context) {
return {
// Listen for variable declarations
JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
context.report({
node,
messageId: "errorMessage"
});
}
};
}
});

export default rule;
`;
module.exports = ruleBoilerplate;
24 changes: 24 additions & 0 deletions scripts/boilerplate/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

const testBoilerplate = name => `// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { Rule } from "eslint";
import ruleTester from "./helper/ruleTester";
import rule from "../../../lib/rules/${name}";

// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------

ruleTester.run("${name}", rule as unknown as Rule.RuleModule, {
valid: [
/* ... */
],
invalid: [
/* ... */
]
});
`;
module.exports = testBoilerplate;
Loading
Loading