Skip to content

Commit

Permalink
feat: added unique-dependencies rule (#126)
Browse files Browse the repository at this point in the history
## PR Checklist

-   [x] Addresses an existing open issue: fixes #50
- [x] That issue was marked as [`status: accepting
prs`](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22)
- [x] Steps in
[CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/blob/main/.github/CONTRIBUTING.md)
were taken

## Overview

I ended up naming it `unique-dependencies` because I'm not a fan of
`no-` prefixes for lint rules
(typescript-eslint/typescript-eslint#6022).
  • Loading branch information
JoshuaKGoldberg committed Jan 20, 2024
1 parent ea1336e commit a9417d1
Show file tree
Hide file tree
Showing 6 changed files with 399 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ The default settings don't conflict, and Prettier plugins can quickly fix up ord
| [order-properties](docs/rules/order-properties.md) | Package properties must be declared in standard order || 🔧 | |
| [prefer-repository-shorthand](docs/rules/prefer-repository-shorthand.md) | Enforce shorthand declaration for GitHub repository. || 🔧 | |
| [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) | Enforce that if repository directory is specified, it matches the path to the package.json file || | 💡 |
| [valid-local-dependency](docs/rules/valid-local-dependency.md) | Checks existence of local dependencies in the package.json || | |
| [valid-package-def](docs/rules/valid-package-def.md) | Enforce that package.json has all properties required by the npm spec || | |
| [valid-repository-directory](docs/rules/valid-repository-directory.md) | Enforce that if repository directory is specified, it matches the path to the package.json file || | 💡 |
Expand Down
7 changes: 7 additions & 0 deletions docs/rules/unique-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Enforce that if repository directory is specified, it matches the path to the package.json file (`package-json/unique-dependencies`)

💼 This rule is enabled in the ✅ `recommended` config.

💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).

<!-- end auto-generated rule header -->
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import orderProperties from "./rules/order-properties.js";
import preferRepositoryShorthand from "./rules/prefer-repository-shorthand.js";
import sortCollections from "./rules/sort-collections.js";
import uniqueDependencies from "./rules/unique-dependencies.js";
import validLocalDependency from "./rules/valid-local-dependency.js";
import validPackageDef from "./rules/valid-package-def.js";
import validRepositoryDirectory from "./rules/valid-repository-directory.js";
Expand All @@ -9,6 +10,7 @@ export const rules = {
"order-properties": orderProperties,
"prefer-repository-shorthand": preferRepositoryShorthand,
"sort-collections": sortCollections,
"unique-dependencies": uniqueDependencies,
"valid-local-dependency": validLocalDependency,
"valid-package-def": validPackageDef,
"valid-repository-directory": validRepositoryDirectory,
Expand Down
109 changes: 109 additions & 0 deletions src/rules/unique-dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { AST as JsonAST } from "jsonc-eslint-parser";

import * as ESTree from "estree";

import { createRule } from "../createRule.js";
import { isJSONStringLiteral, isNotNullish } from "../utils/predicates.js";

const dependencyPropertyNames = new Set([
"bundleDependencies",
"bundledDependencies",
"dependencies",
"devDependencies",
"optionalDependencies",
"peerDependencies",
"overrides",
]);

export default createRule({
create(context) {
function check(
elements: (JsonAST.JSONNode | null)[],
getNodeToRemove: (element: JsonAST.JSONNode) => ESTree.Node,
) {
const seen = new Set();

for (const element of elements
.filter(isNotNullish)
.filter(isJSONStringLiteral)
.reverse()) {
if (seen.has(element.value)) {
report(element);
} else {
seen.add(element.value);
}
}

function report(node: JsonAST.JSONNode) {
context.report({
messageId: "overridden",
node: node as unknown as ESTree.Node,
suggest: [
{
fix(fixer) {
const removal = getNodeToRemove(node);
return [
fixer.remove(removal),
fixer.remove(
// A listing that's overridden can't be last,
// so we're guaranteed there's a comma after.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
context.sourceCode.getTokenAfter(
removal,
)!,
),
];
},
messageId: "remove",
},
],
});
}
}

return {
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.type=JSONLiteral]"(
node: JsonAST.JSONProperty & {
key: JsonAST.JSONStringLiteral;
},
) {
if (!dependencyPropertyNames.has(node.key.value)) {
return;
}

switch (node.value.type) {
case "JSONArrayExpression":
check(
node.value.elements,
(element) => element as unknown as ESTree.Node,
);
break;
case "JSONObjectExpression":
check(
node.value.properties.map(
(property) => property.key,
),
(property) =>
property.parent as unknown as ESTree.Node,
);
break;
}
},
};
},

meta: {
docs: {
category: "Best Practices",
description:
"Enforce that if repository directory is specified, it matches the path to the package.json file",
recommended: true,
},
hasSuggestions: true,
messages: {
overridden:
"Package name is overridden by a duplicate listing later on.",
remove: "Remove this redundant dependency listing.",
},
},
});

0 comments on commit a9417d1

Please sign in to comment.