mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(notices): validate SPDX expressions strictly
This commit is contained in:
@@ -105,6 +105,7 @@ External packages **directly declared** only by repository tooling, test infrast
|
||||
| [`@types/picomatch`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/react`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/react-dom`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/spdx-expression-parse`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/turndown`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@typescript-eslint/parser`](https://github.com/typescript-eslint/typescript-eslint) | MIT |
|
||||
| [`@vitejs/plugin-react`](https://github.com/vitejs/vite-plugin-react) | MIT |
|
||||
@@ -131,6 +132,7 @@ External packages **directly declared** only by repository tooling, test infrast
|
||||
| [`playwright`](https://github.com/microsoft/playwright) | Apache-2.0 |
|
||||
| [`publint`](https://github.com/publint/publint) | MIT |
|
||||
| [`smol-toml`](https://github.com/squirrelchat/smol-toml) | BSD-3-Clause |
|
||||
| [`spdx-expression-parse`](https://github.com/jslicense/spdx-expression-parse.js) | MIT |
|
||||
| [`tsdown`](https://github.com/rolldown/tsdown) | MIT |
|
||||
| [`typescript-language-server`](https://github.com/typescript-language-server/typescript-language-server) | Apache-2.0 |
|
||||
| [`vite`](https://github.com/vitejs/vite) | MIT |
|
||||
|
||||
@@ -121,6 +121,7 @@
|
||||
"@types/jsdom": "^28.0.3",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/node": "^22.20.0",
|
||||
"@types/spdx-expression-parse": "^4.0.0",
|
||||
"@typescript-eslint/parser": "8.61.0",
|
||||
"@vitest/coverage-v8": "^4.1.8",
|
||||
"@yarnpkg/cli-dist": "4.17.1",
|
||||
@@ -142,6 +143,7 @@
|
||||
"oxlint-tsgolint": "7.0.2001",
|
||||
"publint": "^0.3.21",
|
||||
"smol-toml": "^1.7.1",
|
||||
"spdx-expression-parse": "^5.0.0",
|
||||
"tsdown": "^0.22.2",
|
||||
"tsx": "^4.22.4",
|
||||
"typescript": "^6.0.3",
|
||||
|
||||
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
@@ -39,6 +39,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^22.20.0
|
||||
version: 22.20.0
|
||||
'@types/spdx-expression-parse':
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
'@typescript-eslint/parser':
|
||||
specifier: 8.61.0
|
||||
version: 8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)
|
||||
@@ -102,6 +105,9 @@ importers:
|
||||
smol-toml:
|
||||
specifier: ^1.7.1
|
||||
version: 1.7.1
|
||||
spdx-expression-parse:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
tsdown:
|
||||
specifier: ^0.22.2
|
||||
version: 0.22.2(oxc-resolver@11.20.0)(publint@0.3.21)(tsx@4.22.4)(typescript@6.0.3)
|
||||
@@ -8728,6 +8734,9 @@ packages:
|
||||
'@types/retry@0.12.0':
|
||||
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
||||
|
||||
'@types/spdx-expression-parse@4.0.0':
|
||||
resolution: {integrity: sha512-odQzy87phelGS4inXOzjmusx4hoCVD0IbxUANxHzVkmTzMRTNnUPoq1urIl7S1qf09KcDWKLFIftPmLtgbsAHA==}
|
||||
|
||||
'@types/tough-cookie@4.0.5':
|
||||
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
|
||||
|
||||
@@ -11090,6 +11099,15 @@ packages:
|
||||
space-separated-tokens@2.0.2:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
spdx-exceptions@2.5.0:
|
||||
resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==}
|
||||
|
||||
spdx-expression-parse@5.0.0:
|
||||
resolution: {integrity: sha512-vngmw3Rgn+o2arXNbnZaj5UtOEBuWBfvaI+Wc8GFfykIhA5/vdK9/Sp/XkLv63dykz2rxKDvKEHupF5P0FORcQ==}
|
||||
|
||||
spdx-license-ids@3.0.23:
|
||||
resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==}
|
||||
|
||||
speakingurl@14.0.1:
|
||||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -13716,6 +13734,8 @@ snapshots:
|
||||
|
||||
'@types/retry@0.12.0': {}
|
||||
|
||||
'@types/spdx-expression-parse@4.0.0': {}
|
||||
|
||||
'@types/tough-cookie@4.0.5': {}
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
@@ -16590,6 +16610,15 @@ snapshots:
|
||||
|
||||
space-separated-tokens@2.0.2: {}
|
||||
|
||||
spdx-exceptions@2.5.0: {}
|
||||
|
||||
spdx-expression-parse@5.0.0:
|
||||
dependencies:
|
||||
spdx-exceptions: 2.5.0
|
||||
spdx-license-ids: 3.0.23
|
||||
|
||||
spdx-license-ids@3.0.23: {}
|
||||
|
||||
speakingurl@14.0.1: {}
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
@@ -181,6 +181,17 @@ describe('isPermissive', () => {
|
||||
// An exception clause is not a recognized identifier, so it fails closed.
|
||||
expect(isPermissive('GPL-2.0-only WITH Classpath-exception-2.0')).toBe(false)
|
||||
})
|
||||
|
||||
it('honors grouping and SPDX precedence', () => {
|
||||
expect(isPermissive('MIT OR (GPL-3.0-only AND GPL-2.0-only)')).toBe(true)
|
||||
expect(isPermissive('(MIT OR Apache-2.0) AND ISC')).toBe(true)
|
||||
})
|
||||
|
||||
it('fails closed for malformed expressions, additions, and exceptions', () => {
|
||||
expect(['MIT)', '((MIT', '(MIT OR GPL-3.0-only', 'MIT OR OR GPL-3.0-only'].some(isPermissive)).toBe(false)
|
||||
expect(isPermissive('MIT+')).toBe(false)
|
||||
expect(isPermissive('GPL-2.0-only WITH Classpath-exception-2.0')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('manifestPatterns', () => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { existsSync, globSync, readdirSync, readFileSync, writeFileSync } from '
|
||||
import { resolve } from 'node:path'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { parse as parseToml, type TomlTableWithoutBigInt, type TomlValueWithoutBigInt } from 'smol-toml'
|
||||
import parseSpdx from 'spdx-expression-parse'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const OUT = 'THIRD_PARTY_NOTICES.md'
|
||||
@@ -433,6 +434,18 @@ function verifyBuildTimePins(): void {
|
||||
/** SPDX identifiers this project may ship without further review. */
|
||||
const PERMISSIVE_LICENSES = new Set(['MIT', 'ISC', 'BSD-2-Clause', 'BSD-3-Clause', 'Apache-2.0', '0BSD', 'Unlicense', 'CC0-1.0', 'BlueOak-1.0.0', 'Python-2.0'])
|
||||
|
||||
/** Evaluate a parsed SPDX expression under the repository's license policy. */
|
||||
function isPermissiveSpdx(expression: ReturnType<typeof parseSpdx>): boolean {
|
||||
if ('conjunction' in expression) {
|
||||
return expression.conjunction === 'and'
|
||||
? isPermissiveSpdx(expression.left) && isPermissiveSpdx(expression.right)
|
||||
: isPermissiveSpdx(expression.left) || isPermissiveSpdx(expression.right)
|
||||
}
|
||||
return expression.plus !== true
|
||||
&& expression.exception === undefined
|
||||
&& PERMISSIVE_LICENSES.has(expression.license)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an SPDX expression grants terms this project may ship under.
|
||||
* `OR` needs one permissive alternative, because the consumer chooses; `AND`
|
||||
@@ -444,15 +457,13 @@ const PERMISSIVE_LICENSES = new Set(['MIT', 'ISC', 'BSD-2-Clause', 'BSD-3-Clause
|
||||
* @returns true when the expression's obligations are all permissive.
|
||||
*/
|
||||
export function isPermissive(license: string): boolean {
|
||||
// npm's legacy dual-license notation predates SPDX `OR`.
|
||||
const normalized = license.replace(/\s*\/\s*/g, ' OR ').replace(/[()]/g, ' ').trim()
|
||||
if (normalized.includes(' AND ')) {
|
||||
return normalized.split(' AND ').every(operand => isPermissive(operand))
|
||||
// Some npm manifests use a slash for a choice despite SPDX requiring `OR`.
|
||||
const normalized = license.replace(/\s*\/\s*/g, ' OR ').trim()
|
||||
try {
|
||||
return isPermissiveSpdx(parseSpdx(normalized))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
if (normalized.includes(' OR ')) {
|
||||
return normalized.split(' OR ').some(operand => isPermissive(operand))
|
||||
}
|
||||
return PERMISSIVE_LICENSES.has(normalized.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user