mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-14 05:06:54 +00:00
chore: publish from main
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
// Shared license validation used by both the external plugin catalog validator
|
||||
// (eng/external-plugin-validation.mjs) and the local plugin.json validator
|
||||
// (eng/validate-plugins.mjs). Keeping it here avoids one validator importing from
|
||||
// the other and gives license rules a single, dedicated home.
|
||||
//
|
||||
// The agent-plugins-spec schema (https://github.com/agentplugins/agent-plugins-spec)
|
||||
// does not enforce SPDX, and plugins may legitimately use proprietary or non-OSS
|
||||
// licenses. A non-SPDX license is therefore a warning, never an error.
|
||||
|
||||
import { inlineCode } from "./markdown.mjs";
|
||||
|
||||
// A single SPDX license identifier token (e.g. "MIT", "Apache-2.0", "LicenseRef-Foo").
|
||||
const SPDX_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9.-]*\+?$/;
|
||||
const SPDX_IDSTRING_PATTERN = /^[A-Za-z0-9.-]+$/;
|
||||
|
||||
// Curated set of common SPDX license identifiers. Not exhaustive: unrecognized
|
||||
// but well-formed identifiers produce a warning rather than an error.
|
||||
const KNOWN_SPDX_IDS = new Set([
|
||||
"0BSD",
|
||||
"AGPL-3.0",
|
||||
"AGPL-3.0-only",
|
||||
"AGPL-3.0-or-later",
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"CC-BY-4.0",
|
||||
"CC-BY-SA-4.0",
|
||||
"CC0-1.0",
|
||||
"EPL-2.0",
|
||||
"GPL-2.0",
|
||||
"GPL-2.0-only",
|
||||
"GPL-2.0-or-later",
|
||||
"GPL-3.0",
|
||||
"GPL-3.0-only",
|
||||
"GPL-3.0-or-later",
|
||||
"ISC",
|
||||
"LGPL-2.1",
|
||||
"LGPL-2.1-only",
|
||||
"LGPL-2.1-or-later",
|
||||
"LGPL-3.0",
|
||||
"LGPL-3.0-only",
|
||||
"LGPL-3.0-or-later",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"Unlicense",
|
||||
"WTFPL",
|
||||
"Zlib",
|
||||
]);
|
||||
|
||||
const KNOWN_SPDX_EXCEPTIONS = new Set([
|
||||
"Classpath-exception-2.0",
|
||||
"GCC-exception-3.1",
|
||||
"LLVM-exception",
|
||||
"Autoconf-exception-3.0",
|
||||
"Bison-exception-2.2",
|
||||
"Font-exception-2.0",
|
||||
"GPL-3.0-linking-exception",
|
||||
"Linux-syscall-note",
|
||||
"OpenSSL-exception",
|
||||
"Qt-GPL-exception-1.0",
|
||||
]);
|
||||
|
||||
function isNonEmptyString(value) {
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
|
||||
function isSpdxIdString(value) {
|
||||
return SPDX_IDSTRING_PATTERN.test(value);
|
||||
}
|
||||
|
||||
function isRecognizedLicenseRef(token) {
|
||||
if (token.startsWith("LicenseRef-")) {
|
||||
return isSpdxIdString(token.slice("LicenseRef-".length));
|
||||
}
|
||||
|
||||
if (!token.startsWith("DocumentRef-")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const separatorIndex = token.indexOf(":LicenseRef-");
|
||||
if (separatorIndex === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const documentRef = token.slice("DocumentRef-".length, separatorIndex);
|
||||
const licenseRef = token.slice(separatorIndex + ":LicenseRef-".length);
|
||||
return isSpdxIdString(documentRef) && isSpdxIdString(licenseRef);
|
||||
}
|
||||
|
||||
function isRecognizedSpdxIdToken(token) {
|
||||
if (isRecognizedLicenseRef(token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!SPDX_ID_PATTERN.test(token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalized = token.endsWith("+") ? token.slice(0, -1) : token;
|
||||
return KNOWN_SPDX_IDS.has(normalized);
|
||||
}
|
||||
|
||||
function isRecognizedSpdxExceptionToken(token) {
|
||||
return isSpdxIdString(token) && KNOWN_SPDX_EXCEPTIONS.has(token);
|
||||
}
|
||||
|
||||
function tokenizeSpdxExpression(license) {
|
||||
return license
|
||||
.replace(/([()])/g, " $1 ")
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((token) => token.length > 0);
|
||||
}
|
||||
|
||||
// Returns true only for a well-formed SPDX license expression composed of
|
||||
// recognized identifiers joined by SPDX operators (optionally parenthesized).
|
||||
// Anything else (proprietary strings, free text, unrecognized ids) returns false so
|
||||
// the caller can warn without rejecting it — the plugin spec does not enforce SPDX.
|
||||
export function isRecognizedSpdxExpression(license) {
|
||||
if (typeof license !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tokens = tokenizeSpdxExpression(license);
|
||||
|
||||
if (tokens.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let position = 0;
|
||||
|
||||
function peek() {
|
||||
return tokens[position];
|
||||
}
|
||||
|
||||
function consume() {
|
||||
return tokens[position++];
|
||||
}
|
||||
|
||||
function parsePrimary() {
|
||||
const token = peek();
|
||||
|
||||
if (token === "(") {
|
||||
consume();
|
||||
if (!parseOrExpression()) {
|
||||
return false;
|
||||
}
|
||||
if (peek() !== ")") {
|
||||
return false;
|
||||
}
|
||||
consume();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!token || token === ")" || ["AND", "OR", "WITH"].includes(token.toUpperCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
consume();
|
||||
return isRecognizedSpdxIdToken(token);
|
||||
}
|
||||
|
||||
function parseSimpleExpression() {
|
||||
const token = peek();
|
||||
if (!token || token === ")" || ["AND", "OR", "WITH"].includes(token.toUpperCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
consume();
|
||||
return isRecognizedSpdxIdToken(token);
|
||||
}
|
||||
|
||||
function parseWithExpression() {
|
||||
if (peek() === "(") {
|
||||
return parsePrimary() && peek()?.toUpperCase() !== "WITH";
|
||||
}
|
||||
|
||||
if (!parseSimpleExpression()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (peek()?.toUpperCase() !== "WITH") {
|
||||
return true;
|
||||
}
|
||||
|
||||
consume();
|
||||
const exceptionToken = peek();
|
||||
if (!exceptionToken || exceptionToken === ")" || ["AND", "OR", "WITH"].includes(exceptionToken.toUpperCase())) {
|
||||
return false;
|
||||
}
|
||||
consume();
|
||||
return isRecognizedSpdxExceptionToken(exceptionToken);
|
||||
}
|
||||
|
||||
function parseAndExpression() {
|
||||
if (!parseWithExpression()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (peek()?.toUpperCase() === "AND") {
|
||||
consume();
|
||||
if (!parseWithExpression()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function parseOrExpression() {
|
||||
if (!parseAndExpression()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (peek()?.toUpperCase() === "OR") {
|
||||
consume();
|
||||
if (!parseAndExpression()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return parseOrExpression() && position === tokens.length;
|
||||
}
|
||||
|
||||
// Canonical license validation. A non-SPDX license is a warning (never an error)
|
||||
// so authors may use proprietary or non-OSS licenses. Returns collected
|
||||
// errors/warnings so each caller can integrate them into its own reporting.
|
||||
//
|
||||
// Options:
|
||||
// required — when true, a missing license is an error (default false).
|
||||
// prefix — optional message prefix (e.g. "external.json[3]") for context.
|
||||
export function validateLicenseField(license, options = {}) {
|
||||
const { required = false } = options;
|
||||
const prefix = options.prefix ? `${options.prefix}: ` : "";
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
if (license === undefined) {
|
||||
if (required) {
|
||||
errors.push(`${prefix}"license" is required`);
|
||||
}
|
||||
return { errors, warnings };
|
||||
}
|
||||
|
||||
if (!isNonEmptyString(license)) {
|
||||
errors.push(`${prefix}"license" must be a non-empty string`);
|
||||
return { errors, warnings };
|
||||
}
|
||||
|
||||
if (!isRecognizedSpdxExpression(license)) {
|
||||
warnings.push(
|
||||
`${prefix}"license" value ${inlineCode(license)} is not a recognized SPDX identifier; prefer a standard SPDX id (https://spdx.org/licenses), though non-SPDX or proprietary licenses are allowed`
|
||||
);
|
||||
}
|
||||
|
||||
return { errors, warnings };
|
||||
}
|
||||
Reference in New Issue
Block a user