Skip to content

Commit

Permalink
feat: added valid-repository-directory rule (#123)
Browse files Browse the repository at this point in the history
## 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`
  • Loading branch information
JoshuaKGoldberg authored Jan 20, 2024
1 parent 928c5b4 commit 84035c3
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 29 deletions.
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,17 @@ module.exports = {

💼 Configurations enabled in.\
✅ Set in the `recommended` configuration.\
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).

| Name                        | Description | 💼 | 🔧 |
| :----------------------------------------------------------------------- | :-------------------------------------------------------------------------------------- | :- | :- |
| [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. || 🔧 |
| [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 || |
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).

| Name                        | Description | 💼 | 🔧 | 💡 |
| :----------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- | :- | :- | :- |
| [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. || 🔧 | |
| [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 || | 💡 |

<!-- end auto-generated rules list -->
<!-- prettier-ignore-end -->
Expand Down
32 changes: 32 additions & 0 deletions docs/rules/valid-repository-directory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Enforce that if repository directory is specified, it matches the path to the package.json file (`package-json/valid-repository-directory`)

💼 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 -->

## Rule Details

This rule enforces that `"repository"` > `"directory"` points to the right directory for a `package.json`.
If `"directory"` isn't specified, this rule will do nothing.

Example of **incorrect** code for this rule for a `package.json` located at `packages/example/package.json`:

```json
{
"repository": {
"directory": "something-else"
}
}
```

Example of **correct** code for this rule for a `package.json` located at `packages/example/package.json`:

```json
{
"repository": {
"directory": "packages/example"
}
}
```
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import preferRepositoryShorthand from "./rules/prefer-repository-shorthand.js";
import sortCollections from "./rules/sort-collections.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";

export const rules = {
"order-properties": orderProperties,
"prefer-repository-shorthand": preferRepositoryShorthand,
"sort-collections": sortCollections,
"valid-local-dependency": validLocalDependency,
"valid-package-def": validPackageDef,
"valid-repository-directory": validRepositoryDirectory,
};

export const configs = {
Expand Down
24 changes: 4 additions & 20 deletions src/rules/prefer-repository-shorthand.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type ESTree from "estree";
import type { AST as JsonAST } from "jsonc-eslint-parser";

import { createRule } from "../createRule.js";
import { findPropertyWithKeyValue } from "../utils/findPropertyWithKeyValue.js";

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

type JSONPropertyWithKeyAndValue<Value extends string> =
JsonAST.JSONProperty & {
key: JsonAST.JSONStringLiteral;
value: Value;
};

function findJSONLiteralWithValue<Value extends string>(
properties: JsonAST.JSONProperty[],
value: Value,
) {
return properties.find(
(property): property is JSONPropertyWithKeyAndValue<Value> =>
property.key.type === "JSONLiteral" && property.key.value === value,
);
}

export default createRule({
create(context) {
return {
Expand All @@ -42,11 +26,11 @@ export default createRule({
if (node.value.type === "JSONObjectExpression") {
const { properties } = node.value;

if (findJSONLiteralWithValue(properties, "directory")) {
if (findPropertyWithKeyValue(properties, "directory")) {
return;
}

const typeProperty = findJSONLiteralWithValue(
const typeProperty = findPropertyWithKeyValue(
properties,
"type",
);
Expand All @@ -57,7 +41,7 @@ export default createRule({
return;
}

const urlProperty = findJSONLiteralWithValue(
const urlProperty = findPropertyWithKeyValue(
properties,
"url",
);
Expand Down
65 changes: 65 additions & 0 deletions src/rules/valid-repository-directory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { AST as JsonAST } from "jsonc-eslint-parser";

import * as ESTree from "estree";
import * as path from "node:path";

import { createRule } from "../createRule.js";
import { findPropertyWithKeyValue } from "../utils/findPropertyWithKeyValue.js";

export default createRule({
create(context) {
return {
"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=repository][value.type=JSONObjectExpression]"(
node: JsonAST.JSONProperty & {
value: JsonAST.JSONObjectExpression;
},
) {
const directoryProperty = findPropertyWithKeyValue(
node.value.properties,
"directory",
);
if (
directoryProperty?.value.type !== "JSONLiteral" ||
typeof directoryProperty.value.value !== "string"
) {
return;
}

const directoryValue = directoryProperty.value.value;
const expected = path.normalize(path.dirname(context.filename));

if (path.normalize(directoryValue) !== expected) {
context.report({
messageId: "mismatched",
node: directoryProperty.value as unknown as ESTree.Node,
suggest: [
{
fix(fixer) {
return fixer.replaceText(
directoryProperty.value as unknown as ESTree.Node,
`"${expected}"`,
);
},
messageId: "replace",
},
],
});
}
},
};
},

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: {
mismatched: "Directory does not match package.json directory.",
replace: "Replace with '{{ expected }}'.",
},
},
});
123 changes: 123 additions & 0 deletions src/tests/rules/valid-repository-directory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import rule from "../../rules/valid-repository-directory.js";
import { ruleTester } from "./ruleTester.js";

ruleTester.run("valid-repository-directory", rule, {
invalid: [
{
code: `{
"repository": {
"directory": "nested/package.json"
}
}
`,
errors: [
{
column: 16,
endColumn: 37,
line: 3,
messageId: "mismatched",
suggestions: [
{
messageId: "replace",
output: `{
"repository": {
"directory": "."
}
}
`,
},
],
},
],
filename: "package.json",
},
{
code: `{
"repository": {
"directory": "incorrect/package.json"
}
}
`,
errors: [
{
column: 16,
endColumn: 40,
line: 3,
messageId: "mismatched",
suggestions: [
{
messageId: "replace",
output: `{
"repository": {
"directory": "correct"
}
}
`,
},
],
},
],
filename: "correct/package.json",
},
{
code: `{
"repository": {
"directory": "incorrect/package.json"
}
}
`,
errors: [
{
column: 16,
endColumn: 40,
line: 3,
messageId: "mismatched",
suggestions: [
{
messageId: "replace",
output: `{
"repository": {
"directory": "deeply/nested"
}
}
`,
},
],
},
],
filename: "deeply/nested/package.json",
},
],
valid: [
`{}`,
`{ "repository": "" }`,
`{ "repository": "JoshuaKGoldberg/eslint-plugin-package-json" }`,
`{ "repository": "https://github.com/JoshuaKGoldberg/eslint-plugin-package-json" }`,
`{ "repository": { "directory": null } }`,
`{ "repository": { "directory": {} } }`,
`{
"repository": {
"directory": 123
}
}
`,
{
code: `{
"repository": {
"directory": "nested"
}
}
`,
filename: "nested/package.json",
},
{
code: `{
"repository": {
"directory": "deeply/nested"
}
}
`,
filename: "deeply/nested/package.json",
},
],
});
17 changes: 17 additions & 0 deletions src/utils/findPropertyWithKeyValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { AST as JsonAST } from "jsonc-eslint-parser";

export type JSONPropertyWithKeyAndValue<Value extends string> =
JsonAST.JSONProperty & {
key: JsonAST.JSONStringLiteral;
value: Value;
};

export function findPropertyWithKeyValue<Value extends string>(
properties: JsonAST.JSONProperty[],
value: Value,
) {
return properties.find(
(property): property is JSONPropertyWithKeyAndValue<Value> =>
property.key.type === "JSONLiteral" && property.key.value === value,
);
}

0 comments on commit 84035c3

Please sign in to comment.