Repository: JoshuaKGoldberg/eslint-plugin-package-json (ts)

Issue:
🚀 Feature: Bring in a valid-license rule
### Bug Report Checklist

- [x] I have tried restarting my IDE and the issue persists.
- [x] I have pulled the latest `main` branch of the repository.
- [x] I have [searched for related issues](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json/issues?q=is%3Aissue) and found none that matched my issue.

### Overview

Following #42 -> #51: we've resolved to create a standalone `valid-license` rule. It should enforce that, _if_ the field exists in the `package.json` file, it's set to a valid value. It should do nothing if the field doesn't exist.

### Additional Info

Per discussion in #51, this won't be enabled by default at first. For this issue, we should:

1. Send a PR to create the rule in this repo (resolving this issue)
2. File an issue on https://github.com/JoshuaKGoldberg/package-json-validator to create a standalone API that performs the same validation
3. Once a version of `package-json-validator` is released with that feature, file an issue on this repo to switch the rule to use that API from `package-json-validator`

If you want to take on just step 1, that's ok - no pressure to do all three. 🙂 

Once _all_ `valid-` rules exist in this plugin, we can then file an issue to deprecate the `valid-package-def` rule in favor of them.

💖

Pull request:
feat(valid-license): add new rule for validating `license`
<!-- 👋 Hi, thanks for sending a PR to eslint-plugin-package-json! 🗂
Please fill out all fields below and make sure each item is true and [x] checked.
Otherwise we may not be able to review your PR. -->

## PR Checklist

- [x] Addresses an existing open issue: fixes #830
- [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

This change adds a new `valid-license` rule that uses `validateLicense` from package-json-validator to identify errors.


Patch:
diff --git a/README.md b/README.md
index 8fe4711..781e6ba 100644
--- a/README.md
+++ b/README.md
@@ -140,6 +140,7 @@ The default settings don't conflict, and Prettier plugins can quickly fix up ord
 | [valid-author](docs/rules/valid-author.md)                             | Enforce that the author field is a valid npm author specification                                 | ✔️ ✅ |    |    |    |
 | [valid-bin](docs/rules/valid-bin.md)                                   | Enforce that the `bin` property is valid.                                                         | ✔️ ✅ |    | 💡 |    |
 | [valid-bundleDependencies](docs/rules/valid-bundleDependencies.md)     | Enforce that the `bundleDependencies` (or `bundledDependencies`) property is valid.               | ✔️ ✅ |    |    |    |
+| [valid-license](docs/rules/valid-license.md)                           | Enforce that the `license` property is valid.                                                     | ✔️ ✅ |    |    |    |
 | [valid-local-dependency](docs/rules/valid-local-dependency.md)         | Checks existence of local dependencies in the package.json                                        |      |    |    | ❌  |
 | [valid-name](docs/rules/valid-name.md)                                 | Enforce that package names are valid npm package names                                            | ✔️ ✅ |    |    |    |
 | [valid-package-definition](docs/rules/valid-package-definition.md)     | Enforce that package.json has all properties required by the npm spec                             | ✔️ ✅ |    |    |    |
diff --git a/docs/rules/valid-license.md b/docs/rules/valid-license.md
new file mode 100644
index 0000000..b326a2d
--- /dev/null
+++ b/docs/rules/valid-license.md
@@ -0,0 +1,28 @@
+# valid-license
+
+💼 This rule is enabled in the following configs: ✔️ `legacy-recommended`, ✅ `recommended`.
+
+<!-- end auto-generated rule header -->
+
+This rule does the following checks on the value of the `license` property:
+
+- It must be a `string`
+- The value should be a valid SPDX license, "UNLICENSED", or a note referencing a license file (e.g. "SEE LICENSE IN LICENSE.md")
+
+Example of **incorrect** code for this rule:
+
+```json
+{
+	"license": "GPL3"
+}
+```
+
+Example of **correct** code for this rule:
+
+```json
+{
+	"license": "GPL-3.0-only"
+}
+```
+
+**See also:** [SPDX Documentation](https://spdx.org/licenses/)
diff --git a/package.json b/package.json
index bdb445f..02629dc 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
 		"detect-indent": "^7.0.1",
 		"detect-newline": "^4.0.1",
 		"eslint-fix-utils": "~0.4.0",
-		"package-json-validator": "~0.20.0",
+		"package-json-validator": "~0.22.0",
 		"semver": "^7.5.4",
 		"sort-object-keys": "^1.1.3",
 		"sort-package-json": "^3.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4f4a70e..21098bd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -24,8 +24,8 @@ importers:
         specifier: ~0.4.0
         version: 0.4.0(@types/estree@1.0.7)(eslint@9.30.0(jiti@2.4.2))
       package-json-validator:
-        specifier: ~0.20.0
-        version: 0.20.1
+        specifier: ~0.22.0
+        version: 0.22.0
       semver:
         specifier: ^7.5.4
         version: 7.7.2
@@ -3183,8 +3183,8 @@ packages:
   package-json-from-dist@1.0.1:
     resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
 
-  package-json-validator@0.20.1:
-    resolution: {integrity: sha512-ZFGIYroTnzozNtDpJDaN9w7bq0ssMSWA2Ea751JeUXdu2MBf0pd+Dhk+s9Q34ST9M7RZuwp4eqIASt59qZ2vIQ==}
+  package-json-validator@0.22.0:
+    resolution: {integrity: sha512-XzCbXfWUhQX60ONIYuJQQ1GWXJLVSD17XNFjU+fk0ckihpsoOc0TZ685x5yb3cAi8W0vt52icQCsnQXZUOZrkQ==}
     engines: {node: ^20.19.0 || >=22.12.0}
     hasBin: true
 
@@ -7411,8 +7411,9 @@ snapshots:
 
   package-json-from-dist@1.0.1: {}
 
-  package-json-validator@0.20.1:
+  package-json-validator@0.22.0:
     dependencies:
+      validate-npm-package-license: 3.0.4
       yargs: 18.0.0
 
   parent-module@1.0.1:
diff --git a/src/plugin.ts b/src/plugin.ts
index 5e06636..e2e28d5 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -15,6 +15,7 @@ import { rule as uniqueDependencies } from "./rules/unique-dependencies.js";
 import { rule as validAuthor } from "./rules/valid-author.js";
 import { rule as validBin } from "./rules/valid-bin.js";
 import { rule as validBundleDependencies } from "./rules/valid-bundleDependencies.js";
+import { rule as validLicense } from "./rules/valid-license.js";
 import { rule as validLocalDependency } from "./rules/valid-local-dependency.js";
 import { rule as validName } from "./rules/valid-name.js";
 import { rule as validPackageDefinition } from "./rules/valid-package-definition.js";
@@ -42,6 +43,7 @@ const rules: Record<string, PackageJsonRuleModule> = {
 	"valid-author": validAuthor,
 	"valid-bin": validBin,
 	"valid-bundleDependencies": validBundleDependencies,
+	"valid-license": validLicense,
 	"valid-local-dependency": validLocalDependency,
 	"valid-name": validName,
 	"valid-package-definition": validPackageDefinition,
diff --git a/src/rules/valid-license.ts b/src/rules/valid-license.ts
new file mode 100644
index 0000000..69f1cf7
--- /dev/null
+++ b/src/rules/valid-license.ts
@@ -0,0 +1,44 @@
+import { AST as JsonAST } from "jsonc-eslint-parser";
+import { validateLicense } from "package-json-validator";
+
+import { createRule } from "../createRule.js";
+import { formatErrors } from "../utils/formatErrors.js";
+
+export const rule = createRule({
+	create(context) {
+		return {
+			"Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=license]"(
+				node: JsonAST.JSONProperty,
+			) {
+				const valueNode = node.value;
+				const value: unknown = JSON.parse(
+					context.sourceCode.getText(valueNode),
+				);
+
+				const errors = validateLicense(value);
+				if (errors.length) {
+					context.report({
+						data: {
+							errors: formatErrors(errors),
+						},
+						messageId: "validationError",
+						node: valueNode,
+					});
+				}
+			},
+		};
+	},
+
+	meta: {
+		docs: {
+			category: "Best Practices",
+			description: "Enforce that the `license` property is valid.",
+			recommended: true,
+		},
+		messages: {
+			validationError: "Invalid license: {{ errors }}",
+		},
+		schema: [],
+		type: "problem",
+	},
+});


Test patch:
diff --git a/src/tests/rules/valid-license.test.ts b/src/tests/rules/valid-license.test.ts
new file mode 100644
index 0000000..6bb4fdb
--- /dev/null
+++ b/src/tests/rules/valid-license.test.ts
@@ -0,0 +1,69 @@
+import { rule } from "../../rules/valid-license.js";
+import { ruleTester } from "./ruleTester.js";
+
+ruleTester.run("valid-license", rule, {
+	invalid: [
+		{
+			code: `{
+	"license": null
+}
+`,
+			errors: [
+				{
+					data: {
+						errors: "the field is `null`, but should be a `string`",
+					},
+					messageId: "validationError",
+				},
+			],
+		},
+		{
+			code: `{
+	"license": 123
+}
+`,
+			errors: [
+				{
+					data: {
+						errors: "the type should be a `string`, not `number`",
+					},
+					messageId: "validationError",
+				},
+			],
+		},
+		{
+			code: `{
+	"license": ""
+}
+`,
+			errors: [
+				{
+					data: {
+						errors: "the value is empty, but should be a valid license",
+					},
+					messageId: "validationError",
+				},
+			],
+		},
+		{
+			code: `{
+	"license": "not-a-license"
+}
+`,
+			errors: [
+				{
+					data: {
+						errors: 'license should be a valid SPDX license expression (without "LicenseRef"), "UNLICENSED", or "SEE LICENSE IN <filename>"',
+					},
+					messageId: "validationError",
+				},
+			],
+		},
+	],
+	valid: [
+		"{}",
+		`{ "license": "MIT" }`,
+		`{ "license": "UNLICENSED" }`,
+		`{ "license": "SEE LICENSE IN LICENSE.md" }`,
+	],
+});