From 7019c1bad48740d84067ad5896900ee58962607d Mon Sep 17 00:00:00 2001 From: xenobytezero Date: Tue, 28 Jan 2025 16:21:06 +0000 Subject: [PATCH] Add the "require-licence" rule --- docs/rules/require-license.md | 76 +++++++++++++++++++++++ src/plugin.ts | 2 + src/rules/require-license.ts | 80 +++++++++++++++++++++++++ src/tests/rules/require-license.test.ts | 62 +++++++++++++++++++ 4 files changed, 220 insertions(+) create mode 100644 docs/rules/require-license.md create mode 100644 src/rules/require-license.ts create mode 100644 src/tests/rules/require-license.test.ts diff --git a/docs/rules/require-license.md b/docs/rules/require-license.md new file mode 100644 index 00000000..4d1af285 --- /dev/null +++ b/docs/rules/require-license.md @@ -0,0 +1,76 @@ +# require-license + +💼 This rule is enabled in the ✅ `recommended` config. + + + +This rule applies two validations to the `"licence"` 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 + +```ts +{ + "require-license": ["error", "GPL"] +} +``` + +```json +{ + "license": "MIT" +} +``` + +When the rule is configured with + +```ts +{ + "require-license": ["error", ["MIT", "GPL"]] +} +``` + +```json +{ + "license": "Apache" +} +``` + +Example of **correct** code for this rule: + +When the rule is configured with + +```ts +{ + "require-license": ["error", "GPL"] +} +``` + +```json +{ + "license": "GPL" +} +``` + +When the rule is configured with + +```ts +{ + "require-license": ["error", ["Apache", "MIT"]] +} +``` + +```json +{ + "license": "Apache" +} +``` + +```json +{ + "license": "MIT" +} +``` diff --git a/src/plugin.ts b/src/plugin.ts index a09d3077..6116bb3f 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -6,6 +6,7 @@ import { rule as noEmptyFields } from "./rules/no-empty-fields.js"; import { rule as noRedundantFiles } from "./rules/no-redundant-files.js"; import { rule as orderProperties } from "./rules/order-properties.js"; import { rule as preferRepositoryShorthand } from "./rules/repository-shorthand.js"; +import { rule as requireLicense } from "./rules/require-license.js"; import { rule as sortCollections } from "./rules/sort-collections.js"; import { rule as uniqueDependencies } from "./rules/unique-dependencies.js"; import { rule as validLocalDependency } from "./rules/valid-local-dependency.js"; @@ -26,6 +27,7 @@ const rules: Record = { "no-redundant-files": noRedundantFiles, "order-properties": orderProperties, "repository-shorthand": preferRepositoryShorthand, + "require-license": requireLicense, "sort-collections": sortCollections, "unique-dependencies": uniqueDependencies, "valid-local-dependency": validLocalDependency, diff --git a/src/rules/require-license.ts b/src/rules/require-license.ts new file mode 100644 index 00000000..029b2ac4 --- /dev/null +++ b/src/rules/require-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/require-license.test.ts b/src/tests/rules/require-license.test.ts new file mode 100644 index 00000000..644dc5e0 --- /dev/null +++ b/src/tests/rules/require-license.test.ts @@ -0,0 +1,62 @@ +import { generateReportData, rule } from "../../rules/require-license.js"; +import { ruleTester } from "./ruleTester.js"; + +ruleTester.run("requires-license", rule, { + invalid: [ + { + code: JSON.stringify({ + license: "CC BY-SA", + name: "some-test-package", + }), + errors: [ + { + data: generateReportData(["GPL"]), + messageId: "invalidValue", + }, + ], + options: ["GPL"], + }, + { + code: JSON.stringify({ + license: "Apache", + name: "some-test-package", + }), + errors: [ + { + data: generateReportData(["MIT", "GPL"]), + messageId: "invalidValue", + }, + ], + options: [["MIT", "GPL"]], + }, + { + code: JSON.stringify({ + license: 1234, + name: "some-test-package", + }), + errors: [ + { + data: generateReportData(["GPL"]), + messageId: "nonString", + }, + ], + options: ["GPL"], + }, + ], + valid: [ + { + code: JSON.stringify({ + license: "Apache", + name: "some-test-package", + }), + options: ["Apache"], + }, + { + code: JSON.stringify({ + license: "GPL", + name: "some-test-package", + }), + options: [["MIT", "GPL"]], + }, + ], +});