diff --git a/docs/rules/valid-license.md b/docs/rules/valid-license.md new file mode 100644 index 00000000..40e37aaa --- /dev/null +++ b/docs/rules/valid-license.md @@ -0,0 +1,76 @@ +# valid-license + +💼 This rule is enabled in the ✅ `recommended` config. + + + +This rule applies two validations to the `"license"` property: + +- It must be a string rather than any other data type +- It's value should match one of the values provided in the options + +Example of **incorrect** code for this rule: + +When the rule is configured with + +```json +{ + "valid-license": ["error", "GPL"] +} +``` + +```json +{ + "license": "MIT" +} +``` + +When the rule is configured with + +```json +{ + "valid-license": ["error", ["MIT", "GPL"]] +} +``` + +```json +{ + "license": "Apache" +} +``` + +Example of **correct** code for this rule: + +When the rule is configured with + +```json +{ + "valid-license": ["error", "GPL"] +} +``` + +```json +{ + "license": "GPL" +} +``` + +When the rule is configured with + +```json +{ + "valid-license": ["error", ["Apache", "MIT"]] +} +``` + +```json +{ + "license": "Apache" +} +``` + +```json +{ + "license": "MIT" +} +``` diff --git a/src/plugin.ts b/src/plugin.ts index a09d3077..565228bf 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -8,6 +8,7 @@ import { rule as orderProperties } from "./rules/order-properties.js"; import { rule as preferRepositoryShorthand } from "./rules/repository-shorthand.js"; import { rule as sortCollections } from "./rules/sort-collections.js"; import { rule as uniqueDependencies } from "./rules/unique-dependencies.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"; @@ -17,48 +18,49 @@ import { rule as validVersion } from "./rules/valid-version.js"; const require = createRequire(import.meta.url || __filename); const { name, version } = require("../package.json") as { - name: string; - version: string; + name: string; + version: string; }; const rules: Record = { - "no-empty-fields": noEmptyFields, - "no-redundant-files": noRedundantFiles, - "order-properties": orderProperties, - "repository-shorthand": preferRepositoryShorthand, - "sort-collections": sortCollections, - "unique-dependencies": uniqueDependencies, - "valid-local-dependency": validLocalDependency, - "valid-name": validName, - "valid-package-definition": validPackageDefinition, - "valid-repository-directory": validRepositoryDirectory, - "valid-version": validVersion, + "no-empty-fields": noEmptyFields, + "no-redundant-files": noRedundantFiles, + "order-properties": orderProperties, + "repository-shorthand": preferRepositoryShorthand, + "sort-collections": sortCollections, + "unique-dependencies": uniqueDependencies, + "valid-license": validLicense, + "valid-local-dependency": validLocalDependency, + "valid-name": validName, + "valid-package-definition": validPackageDefinition, + "valid-repository-directory": validRepositoryDirectory, + "valid-version": validVersion, - /** @deprecated use 'valid-package-definition' instead */ - "valid-package-def": { - ...validPackageDefinition, - meta: { - ...validPackageDefinition.meta, - deprecated: true, - docs: { - ...validPackageDefinition.meta.docs, - recommended: false, - }, - replacedBy: ["valid-package-definition"], - }, - }, + /** @deprecated use 'valid-package-definition' instead */ + "valid-package-def": { + ...validPackageDefinition, + meta: { + ...validPackageDefinition.meta, + deprecated: true, + docs: { + ...validPackageDefinition.meta.docs, + recommended: false, + }, + replacedBy: ["valid-package-definition"], + }, + }, }; export const plugin = { - meta: { - name, - version, - }, - rules, + meta: { + name, + version, + }, + rules, }; export const recommendedRuleSettings = Object.fromEntries( - Object.entries(rules) - .filter(([, rule]) => rule.meta.docs?.recommended) - .map(([name]) => ["package-json/" + name, "error" as const]), + Object.entries(rules) + .filter(([, rule]) => rule.meta.docs?.recommended) + .map(([name]) => ["package-json/" + name, "error" as const]), ); diff --git a/src/rules/valid-license.ts b/src/rules/valid-license.ts new file mode 100644 index 00000000..029b2ac4 --- /dev/null +++ b/src/rules/valid-license.ts @@ -0,0 +1,80 @@ +import type { AST as JsonAST } from "jsonc-eslint-parser"; + +import * as ESTree from "estree"; + +import { createRule } from "../createRule.js"; + +export type AllowedValues = string | string[]; +export type Options = [AllowedValues]; + +export function generateReportData(allowed: string[]) { + return { + allowed: allowed.map((v) => `'${v}'`).join(","), + multiple: allowed.length > 1 ? "one of" : "", + }; +} + +export const rule = createRule({ + create(context) { + const ruleOptions = context.options[0]; + const allowedValues = Array.isArray(ruleOptions) + ? ruleOptions + : [ruleOptions]; + return { + "Program > JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=license]"( + node: JsonAST.JSONProperty, + ) { + if ( + node.value.type !== "JSONLiteral" || + typeof node.value.value !== "string" + ) { + context.report({ + messageId: "nonString", + node: node.value as unknown as ESTree.Node, + }); + return; + } + + if (!allowedValues.includes(node.value.value)) { + context.report({ + data: generateReportData(allowedValues), + loc: node.loc, + messageId: "invalidValue", + }); + } + }, + }; + }, + + meta: { + docs: { + category: "Best Practices", + description: + "Enforce the 'license' field to be a specific value/values", + recommended: false, + }, + messages: { + invalidValue: '"license" must be{{multiple}} {{allowed}}"', + nonString: '"license" must be a string"', + }, + schema: { + items: [ + { + oneOf: [ + { + items: { + type: "string", + }, + type: "array", + }, + { + type: "string", + }, + ], + }, + ], + type: "array", + }, + type: "problem", + }, +}); diff --git a/src/tests/rules/valid-license.test.ts b/src/tests/rules/valid-license.test.ts new file mode 100644 index 00000000..fae2cffb --- /dev/null +++ b/src/tests/rules/valid-license.test.ts @@ -0,0 +1,74 @@ +import { generateReportData, rule } from "../../rules/valid-license.js"; +import { ruleTester } from "./ruleTester.js"; + +ruleTester.run("valid-license", rule, { + invalid: [ + // Invalid with single valid value + { + code: JSON.stringify({ + license: "CC BY-SA", + name: "some-test-package", + }), + errors: [ + { + data: generateReportData(["GPL"]), + messageId: "invalidValue", + }, + ], + options: ["GPL"], + }, + // Invalid with multiple valid values + { + code: JSON.stringify({ + license: "Apache", + name: "some-test-package", + }), + errors: [ + { + data: generateReportData(["MIT", "GPL"]), + messageId: "invalidValue", + }, + ], + options: [["MIT", "GPL"]], + }, + // Invalid property type + { + code: JSON.stringify({ + license: 1234, + name: "some-test-package", + }), + errors: [ + { + data: generateReportData(["GPL"]), + messageId: "nonString", + }, + ], + options: ["GPL"], + }, + ], + valid: [ + // Valid value from single valid value + { + code: JSON.stringify({ + license: "Apache", + name: "some-test-package", + }), + options: ["Apache"], + }, + // Valid value from multiple valid values + { + code: JSON.stringify({ + license: "GPL", + name: "some-test-package", + }), + options: [["MIT", "GPL"]], + }, + // Missing property + { + code: JSON.stringify({ + name: "some-test-package", + }), + options: [["MIT", "GPL"]], + }, + ], +});