Repository navigation
Evaluate SPDX license expressions instead of plain identifiers - #311
Merged
Merged
Conversation
dennisdoomen
commented
Oct 25, 2026
| /// Folds this expression into a single value, using <paramref name="term"/> for every single license and | ||
| /// <paramref name="or"/> and <paramref name="and"/> to combine the operands of compound expressions. | ||
| /// </summary> | ||
| public T Evaluate<T>(Func<LicenseExpression, T> term, Func<T, T, T> or, Func<T, T, T> and) => Operator switch |
Owner
Author
There was a problem hiding this comment.
💪🤖 Nice: one Evaluate fold drives allow, deny, warn, legal risk and the classifier.
Owner
Author
|
🏕️🤖 Only |
Licenses were compared as whole strings, so a package with "MIT OR Apache-2.0" failed an allow list that has both MIT and Apache-2.0. A small parser now reads OR, AND, WITH, parentheses and the "+" suffix, and replaces deprecated GNU identifiers such as GPL-3.0 with their current form. Text that is not a valid expression is matched as is. - Allow: OR needs one allowed option, AND needs all. A WITH term is allowed by the full term or by its base license. - Deny and warn: OR needs every option denied, AND needs one. A WITH term is only denied by the full term. - The reason names the license that matched, for example 'matches allow list license entry "MIT" (from "MIT OR Apache-2.0")'. - Legal risk and the init classifier use the least restrictive option of an OR and the most restrictive license of an AND. Closes #217 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The legal risk evaluator checked for restrictive licenses first, using a substring match on "GPL". Because "LGPL" contains "GPL", every LGPL license got the restrictive score of 6.0 instead of 3.0. Also mention license expressions in the risk metrics page, the intro and the README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dennisdoomen
force-pushed
the
claude/217-spdx-expressions
branch
from
October 25, 2026 16:26
c99a639 to
6b22540
Compare
Coverage Report for CI Build 40211711527Coverage decreased (-0.4%) to 80.772%Details
Uncovered ChangesNo uncovered changes found. Coverage Regressions50 previously-covered lines in 4 files lost coverage.
Coverage Stats💛 - Coveralls |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #217
Problem
Licenses were compared as whole strings. A package that declares
MIT OR Apache-2.0failed an allow list that has bothMITandApache-2.0.What changed
A new internal
LicenseExpressionclass parses SPDX expressions:OR,AND,WITH, parentheses and the+suffix. Operators are not case-sensitive. Text that is not a valid expression, such asMicrosoft .NET Library LicenseorUnknown, is still matched as a whole string, as before.A OR BA AND BA WITH EA WITH EorAis allowedA WITH Eis deniedA+,A-or-laterOther changes:
"GPL-2.0-only WITH Classpath-exception-2.0".GPL-3.0becomesGPL-3.0-onlyandGPL-2.0+becomesGPL-2.0-or-later. This applies to package licenses and to config entries.matches allow list license entry "MIT" (from "MIT OR Apache-2.0").LegalRiskEvaluatoruses the parsed expression. Before, it split the license on" OR ". Now it scores the least restrictive option of anORand the most restrictive license of anAND.LicenseClassifier, used byinit, follows the same rules. It now also recognises deprecated identifiers.CSharpProjectAnalysisStrategy.UpdateLicensePolicyCompatibilityhad its own copy of the license check. It now uses the same matching.Why no library
The issue suggested a library. NuGet.Packaging's
NuGetLicenseExpression.Parseis already in the graph as a transitive dependency, so I tried it on real license values. It accepts unknown ids, but it throws on several values that our fetchers and npm packages produce:GPL-3.0,GPL-2.0+MIT or Apache-2.0GPL-2.0-only WITH Unknown-exceptionMicrosoft .NET Library License,MIT ORSome-Unknown-Id,mit,Apache-2.0+So we would still have to rewrite tokens before parsing and keep a fallback for free text. A small recursive descent parser (about 60 lines) does all of that in one place.
Also fixed
LegalRiskEvaluatorscored everyLGPL-*license as restrictive (6.0), because the substring check for"GPL"ran before the weak copyleft check. It now checks weak copyleft first, so LGPL scores 3.0.Docs
configuration.md(new "License expressions" table),risk-metrics.md,intro.mdandREADME.md.Tests
LicenseExpressionSpecscovers:OR,ANDand parentheses, for allow and for denyWITHfor allow and for deny+and-or-laterANDRiskEvaluatorSpecs)🤖 Generated with Claude Code