From 81aecf07031247e6a42a640ead4c25680f1e86f9 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sat, 1 Feb 2025 17:47:35 -0600 Subject: [PATCH 1/4] feat(require-author): add new `require-author` rule This change adds the first or our new `require-` rules, and creates some foundational plumbing to make it super easy to add new require rules. I've only done author in this PR. Assuming this all looks good, we can quickly knock all the rest of the require rules in one shot. --- README.md | 1 + docs/rules/require-author.md | 25 +++++++++++++++ package.json | 2 +- src/plugin.ts | 2 ++ src/rules/require-properties.ts | 26 ++++++++++++++++ src/tests/rules/require-author.test.ts | 35 +++++++++++++++++++++ src/utils/createRequirePropertyRule.ts | 42 ++++++++++++++++++++++++++ 7 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 docs/rules/require-author.md create mode 100644 src/rules/require-properties.ts create mode 100644 src/tests/rules/require-author.test.ts create mode 100644 src/utils/createRequirePropertyRule.ts diff --git a/README.md b/README.md index 757457e7..3f2ce72f 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ The default settings don't conflict, and Prettier plugins can quickly fix up ord | [no-redundant-files](docs/rules/no-redundant-files.md) | Prevents adding unnecessary / redundant files. | | | 💡 | | | [order-properties](docs/rules/order-properties.md) | Package properties must be declared in standard order | ✅ | 🔧 | | | | [repository-shorthand](docs/rules/repository-shorthand.md) | Enforce either object or shorthand declaration for repository. | ✅ | 🔧 | | | +| [require-author](docs/rules/require-author.md) | Requires the `author` property to be present. | | | | | | [sort-collections](docs/rules/sort-collections.md) | Dependencies, scripts, and configuration values must be declared in alphabetical order. | ✅ | 🔧 | | | | [unique-dependencies](docs/rules/unique-dependencies.md) | Checks a dependency isn't specified more than once (i.e. in `dependencies` and `devDependencies`) | ✅ | | 💡 | | | [valid-local-dependency](docs/rules/valid-local-dependency.md) | Checks existence of local dependencies in the package.json | ✅ | | | | diff --git a/docs/rules/require-author.md b/docs/rules/require-author.md new file mode 100644 index 00000000..87ea27e9 --- /dev/null +++ b/docs/rules/require-author.md @@ -0,0 +1,25 @@ +# require-author + + + +This rule checks for the existence of the `"author"` property in a package.json, +and reports a violation if it doesn't exist. + +Example of **incorrect** code for this rule: + +```json +{ + "name": "thee-silver-mt-zion", + "version": "13.0.0" +} +``` + +Example of **correct** code for this rule: + +```json +{ + "name": "thee-silver-mt-zion", + "version": "13.0.0", + "author": "Jessica Moss" +} +``` diff --git a/package.json b/package.json index 5fa2e492..3c478113 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "build": "tsup", "format": "prettier \"**/*\" --ignore-unknown", "lint": "eslint . --max-warnings 0", - "lint:eslint-docs": "npm run update:eslint-docs -- --check", + "lint:eslint-docs": "pnpm update:eslint-docs --check", "lint:knip": "knip", "lint:md": "markdownlint \"**/*.md\" \".github/**/*.md\"", "lint:packages": "pnpm dedupe --check", diff --git a/src/plugin.ts b/src/plugin.ts index a09d3077..082027a0 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -6,6 +6,7 @@ import { rule as noEmptyFields } from "./rules/no-empty-fields.js"; import { rule as noRedundantFiles } from "./rules/no-redundant-files.js"; import { rule as orderProperties } from "./rules/order-properties.js"; import { rule as preferRepositoryShorthand } from "./rules/repository-shorthand.js"; +import { rules as requireRules } from "./rules/require-properties.js"; import { rule as sortCollections } from "./rules/sort-collections.js"; import { rule as uniqueDependencies } from "./rules/unique-dependencies.js"; import { rule as validLocalDependency } from "./rules/valid-local-dependency.js"; @@ -25,6 +26,7 @@ const rules: Record = { "no-empty-fields": noEmptyFields, "no-redundant-files": noRedundantFiles, "order-properties": orderProperties, + ...requireRules, "repository-shorthand": preferRepositoryShorthand, "sort-collections": sortCollections, "unique-dependencies": uniqueDependencies, diff --git a/src/rules/require-properties.ts b/src/rules/require-properties.ts new file mode 100644 index 00000000..81f41323 --- /dev/null +++ b/src/rules/require-properties.ts @@ -0,0 +1,26 @@ +import type { PackageJsonRuleModule } from "../createRule.js"; + +import { createRequirePropertyRule } from "../utils/createRequirePropertyRule.js"; + +interface PropertyRule { + isRecommended: boolean; + propertyName: string; +} + +// List of all properties we want to create require- rules for. +const properties = [ + { isRecommended: false, propertyName: "author" }, +] satisfies PropertyRule[]; + +/** All require- flavor of rules */ +const rules: Record = {}; + +// Create all require- rules +for (const { isRecommended, propertyName } of properties) { + rules[`require-${propertyName}`] = createRequirePropertyRule( + propertyName, + isRecommended, + ); +} + +export { rules }; diff --git a/src/tests/rules/require-author.test.ts b/src/tests/rules/require-author.test.ts new file mode 100644 index 00000000..e304fdb1 --- /dev/null +++ b/src/tests/rules/require-author.test.ts @@ -0,0 +1,35 @@ +import { rules } from "../../rules/require-properties.js"; +import { ruleTester } from "./ruleTester.js"; + +ruleTester.run("require-author", rules["require-author"], { + invalid: [ + { + code: "{}", + errors: [ + { + data: { property: "author" }, + line: 1, + messageId: "missing", + }, + ], + }, + { + code: `{ + "name": "foo", + "version": "1.0.0" + } + `, + errors: [ + { + data: { property: "author" }, + line: 1, + messageId: "missing", + }, + ], + }, + ], + valid: [ + `{ "main": "./index.js", "author": "Sophie Trudeau" }`, + `{ "author": "Jessica Moss" }`, + ], +}); diff --git a/src/utils/createRequirePropertyRule.ts b/src/utils/createRequirePropertyRule.ts new file mode 100644 index 00000000..4567d20c --- /dev/null +++ b/src/utils/createRequirePropertyRule.ts @@ -0,0 +1,42 @@ +import { createRule } from "../createRule.js"; + +/** + * Given a top-level property name, create a rule that requires that property to be present. + * Optionally, include it in the recommended config. + */ +export const createRequirePropertyRule = ( + propertyName: string, + isRecommended = false, +) => { + return createRule({ + create(context) { + let hasSeen = false; + return { + [`Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=${propertyName}]`]: + () => { + hasSeen = true; + }, + "Program:exit": () => { + if (!hasSeen) { + context.report({ + data: { property: propertyName }, + messageId: "missing", + node: context.sourceCode.ast, + }); + } + }, + }; + }, + meta: { + docs: { + description: `Requires the \`${propertyName}\` property to be present.`, + recommended: isRecommended, + }, + messages: { + missing: "Property '{{property}}' is required.", + }, + schema: [], + type: "suggestion", + }, + }); +}; From 2138b7e4d534211a6aca58f38bc69b8e37def0ac Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 2 Feb 2025 10:09:01 -0600 Subject: [PATCH 2/4] test(require-author): add more tests --- src/tests/rules/require-author.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/tests/rules/require-author.test.ts b/src/tests/rules/require-author.test.ts index e304fdb1..205ca3d3 100644 --- a/src/tests/rules/require-author.test.ts +++ b/src/tests/rules/require-author.test.ts @@ -18,6 +18,23 @@ ruleTester.run("require-author", rules["require-author"], { "name": "foo", "version": "1.0.0" } + `, + errors: [ + { + data: { property: "author" }, + line: 1, + messageId: "missing", + }, + ], + }, + { + code: `{ + "name": "foo", + "version": "1.0.0", + "bin": { + "author": "./cli.js" + } + } `, errors: [ { @@ -31,5 +48,7 @@ ruleTester.run("require-author", rules["require-author"], { valid: [ `{ "main": "./index.js", "author": "Sophie Trudeau" }`, `{ "author": "Jessica Moss" }`, + `{ "author": 123 }`, + `{ "author": { "name": "Jessica Moss" } }`, ], }); From 753af222544a6f6f1fde6ef6112a55582f1188c1 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 2 Feb 2025 10:17:42 -0600 Subject: [PATCH 3/4] refactor(require-author): simplify check --- src/utils/createRequirePropertyRule.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/utils/createRequirePropertyRule.ts b/src/utils/createRequirePropertyRule.ts index 4567d20c..42d9561f 100644 --- a/src/utils/createRequirePropertyRule.ts +++ b/src/utils/createRequirePropertyRule.ts @@ -1,4 +1,7 @@ +import type { AST as JsonAST } from "jsonc-eslint-parser"; + import { createRule } from "../createRule.js"; +import { isJSONStringLiteral } from "./predicates.js"; /** * Given a top-level property name, create a rule that requires that property to be present. @@ -10,14 +13,17 @@ export const createRequirePropertyRule = ( ) => { return createRule({ create(context) { - let hasSeen = false; return { - [`Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=${propertyName}]`]: - () => { - hasSeen = true; - }, - "Program:exit": () => { - if (!hasSeen) { + "Program > JSONExpressionStatement > JSONObjectExpression"( + node: JsonAST.JSONObjectExpression, + ) { + if ( + !node.properties.some( + (property) => + isJSONStringLiteral(property.key) && + property.key.value === propertyName, + ) + ) { context.report({ data: { property: propertyName }, messageId: "missing", From efd4263457315aaf850799bf9fbea87be6d84230 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 2 Feb 2025 11:54:36 -0600 Subject: [PATCH 4/4] refactor(require-properties): simplify --- src/rules/require-properties.ts | 36 +++++++++++++++------------------ 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/rules/require-properties.ts b/src/rules/require-properties.ts index 81f41323..53eabb9f 100644 --- a/src/rules/require-properties.ts +++ b/src/rules/require-properties.ts @@ -2,25 +2,21 @@ import type { PackageJsonRuleModule } from "../createRule.js"; import { createRequirePropertyRule } from "../utils/createRequirePropertyRule.js"; -interface PropertyRule { - isRecommended: boolean; - propertyName: string; -} - -// List of all properties we want to create require- rules for. +// List of all properties we want to create require- rules for, +// in the format [propertyName, isRecommended] const properties = [ - { isRecommended: false, propertyName: "author" }, -] satisfies PropertyRule[]; - -/** All require- flavor of rules */ -const rules: Record = {}; - -// Create all require- rules -for (const { isRecommended, propertyName } of properties) { - rules[`require-${propertyName}`] = createRequirePropertyRule( - propertyName, - isRecommended, - ); -} + ["author", false], + // TODO: More to come! +] satisfies [string, boolean][]; -export { rules }; +/** All require- flavor rules */ +export const rules = properties.reduce>( + (acc, [propertyName, isRecommended]) => { + acc[`require-${propertyName}`] = createRequirePropertyRule( + propertyName, + isRecommended, + ); + return acc; + }, + {}, +);