Skip to content

Commit 5f42145

Browse files
authored
Merge branch 'main' into user/aubreyquinn/testCoverage
2 parents 9dd2aac + 2fd262d commit 5f42145

12 files changed

+1167
-138
lines changed

.eslintrc.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ module.exports = {
1414
],
1515
plugins: ["header"],
1616
env: {
17-
node: true
17+
node: true,
18+
es6: true
19+
},
20+
parserOptions: {
21+
ecmaVersion: 2021, // Allows the latest ECMAScript features
22+
sourceType: "module" // Ensures `import` and `export` syntax are valid
1823
},
1924
overrides: [
2025
{
@@ -23,7 +28,8 @@ module.exports = {
2328
}
2429
],
2530
rules: {
26-
"header/header": [2, "line", [" Copyright (c) Microsoft Corporation.", " Licensed under the MIT License."], 2]
31+
"header/header": [2, "line", [" Copyright (c) Microsoft Corporation.", " Licensed under the MIT License."], 2],
32+
"no-console": "warn" // Add this to warn about console statements
2733
},
28-
ignorePatterns: ["node_modules", "dist/"]
34+
ignorePatterns: ["node_modules", "dist/", "scripts"]
2935
};

CONTRIBUTING.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any addi
2424

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

27+
- Install [jscodeshift](https://github.com/facebook/jscodeshift) globally.
28+
2729
- **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)
2830
- **External collaborators:** [Fork the repo and clone your fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo)
2931

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

5254
If you want to create a new ESLint rule:
5355

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

6058
## Pull requests
6159

package-lock.json

Lines changed: 770 additions & 128 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
"fix:md": "npm run lint:docs -- --fix",
4848
"test:new": "jest",
4949
"prepare": "husky",
50-
"test:coverage": "jest --coverage"
50+
"test:coverage": "jest --coverage",
51+
"create": "node ./scripts/create-rule"
5152
},
5253
"dependencies": {
5354
"eslint-plugin-header": "^3.1.1",
@@ -72,12 +73,14 @@
7273
"eslint-plugin-node": "^11.1.0",
7374
"husky": "^9.1.6",
7475
"jest": "^29.7.0",
76+
"jscodeshift": "^17.0.0",
7577
"markdownlint": "^0.28.1",
7678
"markdownlint-cli": "^0.33.0",
7779
"npm-run-all": "^4.1.5",
7880
"prettier": "^2.8.4",
7981
"ts-jest": "^29.2.5",
80-
"typescript": "^5.6.2"
82+
"typescript": "^5.6.2",
83+
"yargs": "^17.7.2"
8184
},
8285
"engines": {
8386
"node": "^14.17.0 || ^16.0.0 || >= 18.0.0"

scripts/addRuleToExportIndex.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import fs from "fs";
5+
import { kebabToCamelCase } from "./utils/kebabToKamelCase";
6+
7+
function transformer(file, api, options) {
8+
const j = api.jscodeshift;
9+
const { ruleName, exportIndexFilePath } = options;
10+
11+
// Read the export index file content
12+
const exportIndexSource = j(fs.readFileSync(exportIndexFilePath, "utf8"));
13+
14+
// Convert the rule name to camelCase for the export statement
15+
const exportRuleName = kebabToCamelCase(ruleName);
16+
17+
// Validate ruleName and exportRuleName
18+
if (!ruleName || !exportRuleName) {
19+
throw new Error(`Invalid rule name or export rule name: ${ruleName}, ${exportRuleName}`);
20+
}
21+
22+
// Create the new export specifier
23+
const specifier = j.exportSpecifier.from({
24+
exported: j.identifier(exportRuleName),
25+
local: j.identifier("default")
26+
});
27+
28+
// Create the new export statement
29+
const newExportStatement = j.exportNamedDeclaration(null, [specifier], j.stringLiteral(`./${ruleName}`));
30+
31+
// Find all export statements
32+
const exportStatements = exportIndexSource.find(j.ExportNamedDeclaration);
33+
34+
if (exportStatements.size() === 0) {
35+
// No export statements found, so insert at the beginning of the file
36+
exportIndexSource.get().node.program.body.unshift(newExportStatement);
37+
} else {
38+
// Insert the new export statement after the last one
39+
const lastExportStatement = exportStatements.paths()[exportStatements.size() - 1];
40+
j(lastExportStatement).insertAfter(newExportStatement);
41+
42+
// Re-query the export statements after the insertion
43+
const updatedExportStatements = exportIndexSource.find(j.ExportNamedDeclaration);
44+
45+
// Manually sort the export statements alphabetically
46+
const sortedExports = updatedExportStatements
47+
.nodes()
48+
.map(node => {
49+
if (node.specifiers && node.specifiers[0] && node.specifiers[0].exported) {
50+
return node;
51+
}
52+
return null; // Ignore nodes without valid specifiers
53+
})
54+
.filter(node => node !== null) // Remove nulls
55+
.sort((a, b) => {
56+
const aName = a.specifiers[0].exported.name;
57+
const bName = b.specifiers[0].exported.name;
58+
return aName.localeCompare(bName);
59+
});
60+
61+
// Remove all the original export statements
62+
exportIndexSource.find(j.ExportNamedDeclaration).remove();
63+
64+
// Now insert the sorted export statements back into the AST
65+
const body = exportIndexSource.get().node.program.body;
66+
sortedExports.forEach(exportNode => {
67+
body.push(exportNode); // Insert each export statement at the end of the body
68+
});
69+
}
70+
71+
// Write the modified index file back to the filesystem
72+
fs.writeFileSync(exportIndexFilePath, exportIndexSource.toSource({ quote: "double" }), "utf8");
73+
74+
// Return the original file source (this is for the main file passed in)
75+
return file.source;
76+
}
77+
module.exports = transformer;

scripts/addRuleToIndex.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { kebabToCamelCase } from "./utils/kebabToKamelCase";
5+
6+
// Sort function to keep rules and config sorted alphabetically
7+
const nameSort = (a, b) => {
8+
const aName = a.key.type === "Literal" ? a.key.value : a.key.name;
9+
const bName = a.key.type === "Literal" ? b.key.value : b.key.name;
10+
if (aName < bName) return -1;
11+
if (aName > bName) return 1;
12+
return 0;
13+
};
14+
15+
const transformer = (file, api, options) => {
16+
const j = api.jscodeshift;
17+
const root = j(file.source);
18+
const { ruleName } = options; // No need for rulePath in this case
19+
20+
let changesMade = 0;
21+
22+
// Step 1: Add rule to the `rules` object (without parentheses)
23+
root.find(j.Property, { key: { name: "rules" } })
24+
.at(0)
25+
.forEach(path => {
26+
const properties = path.value.value.properties;
27+
properties.unshift(
28+
j.property("init", j.literal(ruleName), j.memberExpression(j.identifier("rules"), j.identifier(kebabToCamelCase(ruleName))))
29+
);
30+
properties.sort(nameSort);
31+
changesMade += 1;
32+
});
33+
34+
// Step 2: Find and modify `configs.recommended.rules`
35+
root.find(j.Property, { key: { name: "configs" } }).forEach(configPath => {
36+
const recommendedConfig = configPath.value.value.properties.find(prop => prop.key.name === "recommended");
37+
38+
if (recommendedConfig) {
39+
const recommendedRules = recommendedConfig.value.properties.find(prop => prop.key.name === "rules");
40+
41+
if (recommendedRules) {
42+
const rulesProps = recommendedRules.value.properties;
43+
rulesProps.unshift(j.property("init", j.literal(`@microsoft/fluentui-jsx-a11y/${ruleName}`), j.literal("error")));
44+
rulesProps.sort(nameSort);
45+
changesMade += 1;
46+
}
47+
}
48+
});
49+
50+
if (changesMade === 0) {
51+
return null;
52+
}
53+
54+
return root.toSource({ quote: "double", trailingComma: false });
55+
};
56+
module.exports = transformer;

scripts/boilerplate/doc.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
const docBoilerplateGenerator = (name, description) => `# ${description} (@microsoft/fluentui-jsx-a11y/${name})
5+
6+
Write a useful explanation here!
7+
8+
## Rule details
9+
10+
Write more details here!
11+
12+
\`\`\`jsx
13+
<div />
14+
\`\`\`
15+
16+
\`\`\`jsx
17+
<input />
18+
\`\`\`
19+
20+
## Further Reading
21+
`;
22+
module.exports = docBoilerplateGenerator;

scripts/boilerplate/rule.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
const ruleBoilerplate = (name, description) => `// Copyright (c) Microsoft Corporation.
5+
// Licensed under the MIT License.
6+
7+
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
8+
9+
//------------------------------------------------------------------------------
10+
// Rule Definition
11+
//------------------------------------------------------------------------------
12+
13+
// RuleCreator requires a URL or documentation link, but it can be a placeholder
14+
const createRule = ESLintUtils.RuleCreator(name => "https://example.com/rule/${name}");
15+
16+
const rule = createRule({
17+
name: "${name}",
18+
meta: {
19+
type: "suggestion", // could be "problem", "suggestion", or "layout"
20+
docs: {
21+
description: "${description}",
22+
recommended: "error" // could also be "warn"
23+
},
24+
messages: {
25+
errorMessage: "" // describe the issue
26+
},
27+
schema: [] // no options for this rule
28+
},
29+
defaultOptions: [], // no options needed
30+
create(context) {
31+
return {
32+
// Listen for variable declarations
33+
JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
34+
context.report({
35+
node,
36+
messageId: "errorMessage"
37+
});
38+
}
39+
};
40+
}
41+
});
42+
43+
export default rule;
44+
`;
45+
module.exports = ruleBoilerplate;

scripts/boilerplate/test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
const testBoilerplate = name => `// Copyright (c) Microsoft Corporation.
5+
// Licensed under the MIT License.
6+
7+
import { Rule } from "eslint";
8+
import ruleTester from "./helper/ruleTester";
9+
import rule from "../../../lib/rules/${name}";
10+
11+
// -----------------------------------------------------------------------------
12+
// Tests
13+
// -----------------------------------------------------------------------------
14+
15+
ruleTester.run("${name}", rule as unknown as Rule.RuleModule, {
16+
valid: [
17+
/* ... */
18+
],
19+
invalid: [
20+
/* ... */
21+
]
22+
});
23+
`;
24+
module.exports = testBoilerplate;

0 commit comments

Comments
 (0)