diff --git a/src/plugin.ts b/src/plugin.ts index d8b87691..a09d3077 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -2,6 +2,7 @@ import { createRequire } from "node:module"; import type { PackageJsonRuleModule } from "./createRule.js"; +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"; @@ -21,6 +22,7 @@ const { name, version } = require("../package.json") as { }; const rules: Record = { + "no-empty-fields": noEmptyFields, "no-redundant-files": noRedundantFiles, "order-properties": orderProperties, "repository-shorthand": preferRepositoryShorthand, diff --git a/src/rules/no-empty-fields.ts b/src/rules/no-empty-fields.ts new file mode 100644 index 00000000..bcb780e8 --- /dev/null +++ b/src/rules/no-empty-fields.ts @@ -0,0 +1,93 @@ +import type { AST as JsonAST } from "jsonc-eslint-parser"; + +import { createRule } from "../createRule"; +import { isString } from "../utils/predicates"; + +export const rule = createRule({ + create(context) { + const objectFields = [ + "peerDependencies", + "scripts", + "dependencies", + "devDependencies", + ]; + const arrayFields = ["files"]; + + return { + "Program > JSONExpressionStatement > JSONObjectExpression"( + node: JsonAST.JSONObjectExpression, + ) { + function getRange( + properties: JsonAST.JSONProperty[], + property: JsonAST.JSONProperty, + index: number, + ): [number, number] { + const isLastProperty = properties.length - 1 === index; + // if the property is last, we should remove ',' before this property + const start = isLastProperty + ? properties.slice(-2)[0].range[1] + : property.range[0]; + // if the property isn't last, we should remove ',' after this property + const end = property.range[1] + (isLastProperty ? 0 : 1); + return [start, end]; + } + + node.properties.forEach((property, index) => { + if ( + property.key.type === "JSONLiteral" && + isString(property.key.value) && + objectFields.includes(property.key.value) && + property.value.type === "JSONObjectExpression" && + !property.value.properties.length + ) { + context.report({ + data: { + property: property.key.value, + }, + fix(fixer) { + return fixer.removeRange( + getRange(node.properties, property, index), + ); + }, + loc: property.loc, + messageId: "emptyFields", + }); + } else if ( + property.key.type === "JSONLiteral" && + isString(property.key.value) && + arrayFields.includes(property.key.value) && + property.value.type === "JSONArrayExpression" && + !property.value.elements.length + ) { + context.report({ + data: { + property: property.key.value, + }, + fix(fixer) { + return fixer.removeRange( + getRange(node.properties, property, index), + ); + }, + loc: property.loc, + messageId: "emptyFields", + }); + } + }); + }, + }; + }, + meta: { + docs: { + category: "Best Practices", + description: "Remove empty fields", + recommended: true, + }, + hasSuggestions: true, + messages: { + emptyFields: 'Should remove empty "{{property}}"', + }, + fixable: "whitespace", + schema: [], + type: "suggestion", + }, +}); diff --git a/src/utils/predicates.ts b/src/utils/predicates.ts index 55ca95ef..9f6c76cf 100644 --- a/src/utils/predicates.ts +++ b/src/utils/predicates.ts @@ -11,3 +11,7 @@ export function isNotNullish>( ): value is T { return value !== null && value !== undefined; } + +export function isString(value: unknown): value is string { + return typeof value === "string"; +}