Skip to content

Commit 84035c3

Browse files
feat: added valid-repository-directory rule (#123)
## PR Checklist - [x] Addresses an existing open issue: fixes #53 - [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 Equivalent to `json-files/ensure-repository-directory`. Renamed to `valid-*` to match the precedent of the existing rules `valid-package-def` and `valid-package-dependency`. Re-uses the `findJSONLiteralWithValue` utility and renames it to the more appropriate `findPropertyWithKeyValue`
1 parent 928c5b4 commit 84035c3

7 files changed

+254
-29
lines changed

Diff for: README.md

+11-9
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,17 @@ module.exports = {
6969

7070
💼 Configurations enabled in.\
7171
✅ Set in the `recommended` configuration.\
72-
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
73-
74-
| Name                        | Description | 💼 | 🔧 |
75-
| :----------------------------------------------------------------------- | :-------------------------------------------------------------------------------------- | :- | :- |
76-
| [order-properties](docs/rules/order-properties.md) | Package properties must be declared in standard order || 🔧 |
77-
| [prefer-repository-shorthand](docs/rules/prefer-repository-shorthand.md) | Enforce shorthand declaration for GitHub repository. || 🔧 |
78-
| [sort-collections](docs/rules/sort-collections.md) | Dependencies, scripts, and configuration values must be declared in alphabetical order. || 🔧 |
79-
| [valid-local-dependency](docs/rules/valid-local-dependency.md) | Checks existence of local dependencies in the package.json || |
80-
| [valid-package-def](docs/rules/valid-package-def.md) | Enforce that package.json has all properties required by the npm spec || |
72+
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
73+
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
74+
75+
| Name                        | Description | 💼 | 🔧 | 💡 |
76+
| :----------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- | :- | :- | :- |
77+
| [order-properties](docs/rules/order-properties.md) | Package properties must be declared in standard order || 🔧 | |
78+
| [prefer-repository-shorthand](docs/rules/prefer-repository-shorthand.md) | Enforce shorthand declaration for GitHub repository. || 🔧 | |
79+
| [sort-collections](docs/rules/sort-collections.md) | Dependencies, scripts, and configuration values must be declared in alphabetical order. || 🔧 | |
80+
| [valid-local-dependency](docs/rules/valid-local-dependency.md) | Checks existence of local dependencies in the package.json || | |
81+
| [valid-package-def](docs/rules/valid-package-def.md) | Enforce that package.json has all properties required by the npm spec || | |
82+
| [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 || | 💡 |
8183

8284
<!-- end auto-generated rules list -->
8385
<!-- prettier-ignore-end -->

Diff for: docs/rules/valid-repository-directory.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Enforce that if repository directory is specified, it matches the path to the package.json file (`package-json/valid-repository-directory`)
2+
3+
💼 This rule is enabled in the ✅ `recommended` config.
4+
5+
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
6+
7+
<!-- end auto-generated rule header -->
8+
9+
## Rule Details
10+
11+
This rule enforces that `"repository"` > `"directory"` points to the right directory for a `package.json`.
12+
If `"directory"` isn't specified, this rule will do nothing.
13+
14+
Example of **incorrect** code for this rule for a `package.json` located at `packages/example/package.json`:
15+
16+
```json
17+
{
18+
"repository": {
19+
"directory": "something-else"
20+
}
21+
}
22+
```
23+
24+
Example of **correct** code for this rule for a `package.json` located at `packages/example/package.json`:
25+
26+
```json
27+
{
28+
"repository": {
29+
"directory": "packages/example"
30+
}
31+
}
32+
```

Diff for: src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import preferRepositoryShorthand from "./rules/prefer-repository-shorthand.js";
33
import sortCollections from "./rules/sort-collections.js";
44
import validLocalDependency from "./rules/valid-local-dependency.js";
55
import validPackageDef from "./rules/valid-package-def.js";
6+
import validRepositoryDirectory from "./rules/valid-repository-directory.js";
67

78
export const rules = {
89
"order-properties": orderProperties,
910
"prefer-repository-shorthand": preferRepositoryShorthand,
1011
"sort-collections": sortCollections,
1112
"valid-local-dependency": validLocalDependency,
1213
"valid-package-def": validPackageDef,
14+
"valid-repository-directory": validRepositoryDirectory,
1315
};
1416

1517
export const configs = {

Diff for: src/rules/prefer-repository-shorthand.ts

+4-20
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type ESTree from "estree";
2-
import type { AST as JsonAST } from "jsonc-eslint-parser";
32

43
import { createRule } from "../createRule.js";
4+
import { findPropertyWithKeyValue } from "../utils/findPropertyWithKeyValue.js";
55

66
const githubUrlRegex =
77
/^(?:git\+)?(?:ssh:\/\/git@|http?s:\/\/)?(?:www\.)?github\.com\//;
@@ -11,22 +11,6 @@ const isGitHubUrl = (url: string) => githubUrlRegex.test(url);
1111
const cleanGitHubUrl = (url: string) =>
1212
url.replace(githubUrlRegex, "").replace(/\.git$/, "");
1313

14-
type JSONPropertyWithKeyAndValue<Value extends string> =
15-
JsonAST.JSONProperty & {
16-
key: JsonAST.JSONStringLiteral;
17-
value: Value;
18-
};
19-
20-
function findJSONLiteralWithValue<Value extends string>(
21-
properties: JsonAST.JSONProperty[],
22-
value: Value,
23-
) {
24-
return properties.find(
25-
(property): property is JSONPropertyWithKeyAndValue<Value> =>
26-
property.key.type === "JSONLiteral" && property.key.value === value,
27-
);
28-
}
29-
3014
export default createRule({
3115
create(context) {
3216
return {
@@ -42,11 +26,11 @@ export default createRule({
4226
if (node.value.type === "JSONObjectExpression") {
4327
const { properties } = node.value;
4428

45-
if (findJSONLiteralWithValue(properties, "directory")) {
29+
if (findPropertyWithKeyValue(properties, "directory")) {
4630
return;
4731
}
4832

49-
const typeProperty = findJSONLiteralWithValue(
33+
const typeProperty = findPropertyWithKeyValue(
5034
properties,
5135
"type",
5236
);
@@ -57,7 +41,7 @@ export default createRule({
5741
return;
5842
}
5943

60-
const urlProperty = findJSONLiteralWithValue(
44+
const urlProperty = findPropertyWithKeyValue(
6145
properties,
6246
"url",
6347
);

Diff for: src/rules/valid-repository-directory.ts

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { AST as JsonAST } from "jsonc-eslint-parser";
2+
3+
import * as ESTree from "estree";
4+
import * as path from "node:path";
5+
6+
import { createRule } from "../createRule.js";
7+
import { findPropertyWithKeyValue } from "../utils/findPropertyWithKeyValue.js";
8+
9+
export default createRule({
10+
create(context) {
11+
return {
12+
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=repository][value.type=JSONObjectExpression]"(
13+
node: JsonAST.JSONProperty & {
14+
value: JsonAST.JSONObjectExpression;
15+
},
16+
) {
17+
const directoryProperty = findPropertyWithKeyValue(
18+
node.value.properties,
19+
"directory",
20+
);
21+
if (
22+
directoryProperty?.value.type !== "JSONLiteral" ||
23+
typeof directoryProperty.value.value !== "string"
24+
) {
25+
return;
26+
}
27+
28+
const directoryValue = directoryProperty.value.value;
29+
const expected = path.normalize(path.dirname(context.filename));
30+
31+
if (path.normalize(directoryValue) !== expected) {
32+
context.report({
33+
messageId: "mismatched",
34+
node: directoryProperty.value as unknown as ESTree.Node,
35+
suggest: [
36+
{
37+
fix(fixer) {
38+
return fixer.replaceText(
39+
directoryProperty.value as unknown as ESTree.Node,
40+
`"${expected}"`,
41+
);
42+
},
43+
messageId: "replace",
44+
},
45+
],
46+
});
47+
}
48+
},
49+
};
50+
},
51+
52+
meta: {
53+
docs: {
54+
category: "Best Practices",
55+
description:
56+
"Enforce that if repository directory is specified, it matches the path to the package.json file",
57+
recommended: true,
58+
},
59+
hasSuggestions: true,
60+
messages: {
61+
mismatched: "Directory does not match package.json directory.",
62+
replace: "Replace with '{{ expected }}'.",
63+
},
64+
},
65+
});

Diff for: src/tests/rules/valid-repository-directory.test.ts

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import rule from "../../rules/valid-repository-directory.js";
2+
import { ruleTester } from "./ruleTester.js";
3+
4+
ruleTester.run("valid-repository-directory", rule, {
5+
invalid: [
6+
{
7+
code: `{
8+
"repository": {
9+
"directory": "nested/package.json"
10+
}
11+
}
12+
`,
13+
errors: [
14+
{
15+
column: 16,
16+
endColumn: 37,
17+
line: 3,
18+
messageId: "mismatched",
19+
suggestions: [
20+
{
21+
messageId: "replace",
22+
output: `{
23+
"repository": {
24+
"directory": "."
25+
}
26+
}
27+
`,
28+
},
29+
],
30+
},
31+
],
32+
filename: "package.json",
33+
},
34+
{
35+
code: `{
36+
"repository": {
37+
"directory": "incorrect/package.json"
38+
}
39+
}
40+
`,
41+
errors: [
42+
{
43+
column: 16,
44+
endColumn: 40,
45+
line: 3,
46+
messageId: "mismatched",
47+
suggestions: [
48+
{
49+
messageId: "replace",
50+
output: `{
51+
"repository": {
52+
"directory": "correct"
53+
}
54+
}
55+
`,
56+
},
57+
],
58+
},
59+
],
60+
filename: "correct/package.json",
61+
},
62+
{
63+
code: `{
64+
"repository": {
65+
"directory": "incorrect/package.json"
66+
}
67+
}
68+
`,
69+
errors: [
70+
{
71+
column: 16,
72+
endColumn: 40,
73+
line: 3,
74+
messageId: "mismatched",
75+
suggestions: [
76+
{
77+
messageId: "replace",
78+
output: `{
79+
"repository": {
80+
"directory": "deeply/nested"
81+
}
82+
}
83+
`,
84+
},
85+
],
86+
},
87+
],
88+
filename: "deeply/nested/package.json",
89+
},
90+
],
91+
valid: [
92+
`{}`,
93+
`{ "repository": "" }`,
94+
`{ "repository": "JoshuaKGoldberg/eslint-plugin-package-json" }`,
95+
`{ "repository": "https://github.com/JoshuaKGoldberg/eslint-plugin-package-json" }`,
96+
`{ "repository": { "directory": null } }`,
97+
`{ "repository": { "directory": {} } }`,
98+
`{
99+
"repository": {
100+
"directory": 123
101+
}
102+
}
103+
`,
104+
{
105+
code: `{
106+
"repository": {
107+
"directory": "nested"
108+
}
109+
}
110+
`,
111+
filename: "nested/package.json",
112+
},
113+
{
114+
code: `{
115+
"repository": {
116+
"directory": "deeply/nested"
117+
}
118+
}
119+
`,
120+
filename: "deeply/nested/package.json",
121+
},
122+
],
123+
});

Diff for: src/utils/findPropertyWithKeyValue.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { AST as JsonAST } from "jsonc-eslint-parser";
2+
3+
export type JSONPropertyWithKeyAndValue<Value extends string> =
4+
JsonAST.JSONProperty & {
5+
key: JsonAST.JSONStringLiteral;
6+
value: Value;
7+
};
8+
9+
export function findPropertyWithKeyValue<Value extends string>(
10+
properties: JsonAST.JSONProperty[],
11+
value: Value,
12+
) {
13+
return properties.find(
14+
(property): property is JSONPropertyWithKeyAndValue<Value> =>
15+
property.key.type === "JSONLiteral" && property.key.value === value,
16+
);
17+
}

0 commit comments

Comments
 (0)