Skip to content

Commit

Permalink
Add the "require-licence" rule
Browse files Browse the repository at this point in the history
  • Loading branch information
xenobytezero committed Jan 28, 2025
1 parent c955cb4 commit d97ae03
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 34 deletions.
75 changes: 75 additions & 0 deletions docs/rules/require-license.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# require-license

💼 This rule is enabled in the ✅ `recommended` config.

<!-- end auto-generated rule header -->

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"
}
```

70 changes: 36 additions & 34 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 requiresLicense } from "./rules/requires-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";
Expand All @@ -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<string, PackageJsonRuleModule> = {
"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,
"requires-license": requiresLicense,
"sort-collections": sortCollections,
"unique-dependencies": uniqueDependencies,
"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]),
);
85 changes: 85 additions & 0 deletions src/rules/requires-license.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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 convertAllowedValuesToString(allowed: string[]): string {
return allowed.map((v) => `'${v}'`).join(",");
}

export function multipleDescriptor(allowed: string[]) {
return allowed.length > 1 ? 'one of' : ''
}

export const rule = createRule<Options>({
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: {
allowed:
convertAllowedValuesToString(allowedValues),
multiple: multipleDescriptor(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",
},
});
61 changes: 61 additions & 0 deletions src/tests/rules/requires-license.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { RuleTester } from "eslint";

import type { AllowedValues, Options } from "../../rules/requires-license.js";

import {
convertAllowedValuesToString,
multipleDescriptor,
rule,
} from "../../rules/requires-license.js";
import { ruleTester } from "./ruleTester.js";

function createInvalidCase(
licenseValue: unknown,
allowedValues: AllowedValues,
expectedMessageId = "invalidValue",
expectedAllowed: AllowedValues = allowedValues,
): RuleTester.InvalidTestCase {
const base = createValidCase(licenseValue, allowedValues);

const expectedAllowedArr = Array.isArray(expectedAllowed)
? expectedAllowed
: [expectedAllowed];

return {
...base,
errors: [
{
data: {
allowed: convertAllowedValuesToString(expectedAllowedArr),
multiple: multipleDescriptor(expectedAllowedArr),
},
messageId: expectedMessageId,
},
],
};
}

function createValidCase(
licenseValue: unknown,
allowedValues: AllowedValues,
): RuleTester.ValidTestCase {
return {
code: JSON.stringify({
license: licenseValue,
name: "some-test-package",
}),
options: [allowedValues],
};
}

ruleTester.run("requires-license", rule, {
invalid: [
createInvalidCase("CC BY-SA", "GPL"),
createInvalidCase("Apache", ["MIT", "GPL"]),
createInvalidCase(1234, ["GPL"], "nonString"),
],
valid: [
createValidCase("Apache", "Apache"),
createValidCase("GPL", ["MIT", "GPL"]),
],
});

0 comments on commit d97ae03

Please sign in to comment.