blob: 5092b1a7e5af119262c9a51d1ad0a43762034453 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'dart:core' hide RegExp;
import 'dart:io' as system;
import 'cache.dart';
import 'formatter.dart';
import 'limits.dart';
import 'patterns.dart';
import 'regexp_debug.dart';
class FetchedContentsOf extends Key { FetchedContentsOf(super.value); }
enum LicenseType {
afl,
apache,
apacheNotice,
apsl,
bison,
bsd,
eclipse,
freetype,
gpl,
icu,
defaultTemplate, // metatype: a license that applies to a file without an internal license; only used when searching for a license
ietf,
ijg,
lgpl,
libpng,
llvm,
mit,
mpl,
openssl,
unicode,
unknown,
vulkan,
zlib,
}
LicenseType convertLicenseNameToType(String? name) {
switch (name) {
case 'Apache':
case 'Apache-2.0.txt':
case 'LICENSE-APACHE-2.0.txt':
case 'LICENSE.vulkan':
case 'APACHE-LICENSE-2.0':
return LicenseType.apache;
case 'BSD':
case 'BSD-3-Clause.txt':
case 'BSD.txt':
return LicenseType.bsd;
case 'COPYING-LGPL-2.1':
case 'LICENSE-LGPL-2':
case 'LICENSE-LGPL-2.1':
return LicenseType.lgpl;
case 'COPYING-GPL-3':
case 'GPL-3.0-only.txt':
case 'GPLv2.TXT':
return LicenseType.gpl;
case 'FTL.TXT':
return LicenseType.freetype;
case 'zlib.h':
return LicenseType.zlib;
case 'png.h':
return LicenseType.libpng;
case 'ICU':
return LicenseType.icu;
case 'Apple Public Source License':
return LicenseType.apsl;
case 'OpenSSL':
return LicenseType.openssl;
case 'COPYING-MPL-1.1':
case 'LICENSE.MPLv2':
case 'http://mozilla.org/MPL/2.0/':
return LicenseType.mpl;
case 'COPYRIGHT.vulkan':
return LicenseType.vulkan;
case 'LICENSE.MIT':
case 'MIT':
case 'MIT.txt':
return LicenseType.mit;
// file names that don't say what the type is
case 'COPYING':
case 'COPYING.LIB': // lgpl usually
case 'COPYING.RUNTIME': // gcc exception usually
case 'COPYING.txt':
case 'COPYRIGHT.musl':
case 'Copyright':
case 'LICENSE':
case 'LICENSE-APPLE':
case 'LICENSE.TXT':
case 'LICENSE.cssmin':
case 'LICENSE.md':
case 'LICENSE.rst':
case 'LICENSE.txt':
case 'License.txt':
case 'NOTICE':
case 'NOTICE.txt':
case 'copyright':
case 'extreme.indiana.edu.license.TXT':
case 'extreme.indiana.edu.license.txt':
case 'javolution.license.TXT':
case 'javolution.license.txt':
case 'libyaml-license.txt':
case 'license.html':
case 'license.patch':
case 'license.txt':
case 'mh-bsd-gcc':
case 'pivotal.labs.license.txt':
return LicenseType.unknown;
}
throw 'unknown license type: $name';
}
LicenseType convertBodyToType(String body) {
if (body.startsWith(lrApache) && body.contains(lrLLVM)) {
return LicenseType.llvm;
}
if (body.startsWith(lrApache)) {
return LicenseType.apache;
}
if (body.startsWith(lrMPL)) {
return LicenseType.mpl;
}
if (body.startsWith(lrGPL)) {
return LicenseType.gpl;
}
if (body.startsWith(lrAPSL)) {
return LicenseType.apsl;
}
if (body.contains(lrOpenSSL)) {
return LicenseType.openssl;
}
if (body.contains(lrBSD)) {
return LicenseType.bsd;
}
if (body.contains(lrMIT)) {
return LicenseType.mit;
}
if (body.contains(lrZlib)) {
return LicenseType.zlib;
}
if (body.contains(lrPNG)) {
return LicenseType.libpng;
}
if (body.contains(lrBison)) {
return LicenseType.bison;
}
return LicenseType.unknown;
}
// API exposed by the classes in main.dart
abstract class LicenseSource {
String get name;
String get libraryName;
String get officialSourceLocation;
List<License>? nearestLicensesFor(String name);
License? nearestLicenseOfType(LicenseType type);
License? nearestLicenseWithName(String name, { String? authors });
}
// Represents a license/file pairing, with metadata saying where the license came from.
class Assignment {
const Assignment(this.license, this.target, this.source);
final License license;
final String target;
final LicenseSource source;
}
// Represents a group of files assigned to the same license, so that we can avoid
// duplicating licenses in the output.
class GroupedLicense {
GroupedLicense(this.type, this.body);
final LicenseType type;
final String body;
// The names of files to which this license applies.
final Set<String> targets = <String>{};
// The libraries from which those files originate.
final Set<String> libraries = <String>{};
// How we determined the license applied to these files.
final Set<String> origins = <String>{};
String toStringDebug() {
final StringBuffer result = StringBuffer();
result.writeln('=' * 100);
(libraries.map((String s) => 'LIBRARY: $s').toList()..sort()).forEach(result.writeln);
(origins.map((String s) => 'ORIGIN: $s').toList()..sort()).forEach(result.writeln);
result.writeln('TYPE: $type');
(targets.map((String s) => 'FILE: $s').toList()..sort()).forEach(result.writeln);
result.writeln('-' * 100);
if (body.isEmpty) {
result.writeln('<THIS BLOCK INTENTIONALLY LEFT BLANK>');
} else {
result.writeln(body);
}
result.writeln('=' * 100);
return result.toString();
}
String toStringFormal() {
final StringBuffer result = StringBuffer();
(libraries.toList()..sort()).forEach(result.writeln);
result.writeln();
assert(body.isNotEmpty);
result.write(body);
return result.toString();
}
}
List<GroupedLicense> groupLicenses(Iterable<Assignment> assignments) {
final Map<String, GroupedLicense> groups = <String, GroupedLicense>{};
for (final Assignment assignment in assignments) {
final String body = assignment.license.toStringBody(assignment.source);
final GroupedLicense entry = groups.putIfAbsent(body, () => GroupedLicense(assignment.license.type, body));
entry.targets.add(assignment.target);
entry.libraries.add(assignment.source.libraryName);
entry.origins.add(assignment.license.origin);
}
final List<GroupedLicense> results = groups.values.toList();
results.sort((GroupedLicense a, GroupedLicense b) => a.body.compareTo(b.body));
return results;
}
abstract class License {
factory License.unique(String body, LicenseType type, {
bool reformatted = false,
required String origin,
String? authors,
bool yesWeKnowWhatItLooksLikeButItIsNot = false
}) {
if (!reformatted) {
body = reformat(body);
}
final License result = UniqueLicense._(body, type, origin: origin, yesWeKnowWhatItLooksLikeButItIsNot: yesWeKnowWhatItLooksLikeButItIsNot, authors: authors);
assert(() {
if (result is! UniqueLicense || result.type != type) {
throw 'tried to add a UniqueLicense $type, but it was a duplicate of a ${result.runtimeType} ${result.type}';
}
return true;
}());
return result;
}
factory License.template(String body, LicenseType type, {
bool reformatted = false,
required String origin,
String? authors,
}) {
if (!reformatted) {
body = reformat(body);
}
final License result = TemplateLicense._autosplit(body, type, origin: origin, authors: authors);
assert(() {
if (result is! TemplateLicense || result.type != type) {
throw 'tried to add a TemplateLicense $type, but it was a duplicate of a ${result.runtimeType} ${result.type}';
}
return true;
}());
return result;
}
factory License.multiLicense(String body, LicenseType type, {
bool reformatted = false,
String? authors,
required String origin
}) {
if (!reformatted) {
body = reformat(body);
}
final License result = MultiLicense._(body, type, origin: origin, authors: authors);
assert(() {
if (result is! MultiLicense || result.type != type) {
throw 'tried to add a MultiLicense $type, but it was a duplicate of a ${result.runtimeType} ${result.type}';
}
return true;
}());
return result;
}
factory License.message(String body, LicenseType type, {
bool reformatted = false,
required String origin
}) {
if (!reformatted) {
body = reformat(body);
}
final License result = MessageLicense._(body, type, origin: origin);
assert(() {
if (result is! MessageLicense || result.type != type) {
throw 'tried to add a MessageLicense $type, but it was a duplicate of a ${result.runtimeType} ${result.type}';
}
return true;
}());
return result;
}
factory License.blank(String body, LicenseType type, { required String origin }) {
final License result = BlankLicense._(reformat(body), type, origin: origin);
assert(() {
if (result is! BlankLicense || result.type != type) {
throw 'tried to add a BlankLicense $type, but it was a duplicate of a ${result.runtimeType} ${result.type}';
}
return true;
}());
return result;
}
factory License.mozilla(String body, { required String origin }) {
body = reformat(body);
final License result = MozillaLicense._(body, LicenseType.mpl, origin: origin);
assert(() {
if (result is! MozillaLicense) {
throw 'tried to add a MozillaLicense, but it was a duplicate of a ${result.runtimeType} ${result.type}';
}
return true;
}());
return result;
}
factory License.fromMultipleBlocks(List<String> bodies, LicenseType type, {
String? authors,
required String origin,
bool yesWeKnowWhatItLooksLikeButItIsNot = false,
}) {
final String body = bodies.map((String s) => reformat(s)).join('\n\n');
return MultiLicense._(body, type, authors: authors, origin: origin, yesWeKnowWhatItLooksLikeButItIsNot: yesWeKnowWhatItLooksLikeButItIsNot);
}
factory License.fromBodyAndType(String body, LicenseType type, {
bool reformatted = false,
required String origin
}) {
if (!reformatted) {
body = reformat(body);
}
final License result;
switch (type) {
case LicenseType.bsd:
case LicenseType.mit:
result = TemplateLicense._autosplit(body, type, origin: origin);
case LicenseType.apache:
case LicenseType.freetype:
case LicenseType.ijg:
case LicenseType.ietf:
case LicenseType.libpng:
case LicenseType.llvm: // The LLVM license is an Apache variant
case LicenseType.unicode:
case LicenseType.unknown:
case LicenseType.vulkan:
case LicenseType.zlib:
result = MessageLicense._(body, type, origin: origin);
case LicenseType.apacheNotice:
result = UniqueLicense._(body, type, origin: origin);
case LicenseType.mpl:
result = MozillaLicense._(body, type, origin: origin);
// The exception in the license of Bison allows redistributing larger
// works "under terms of your choice"; we choose terms that don't require
// any notice in the binary distribution.
case LicenseType.bison:
result = BlankLicense._(body, type, origin: origin);
case LicenseType.icu:
case LicenseType.openssl:
throw 'Use License.fromMultipleBlocks rather than License.fromBodyAndType for the ICU and OpenSSL licenses.';
case LicenseType.afl:
case LicenseType.apsl:
case LicenseType.eclipse:
case LicenseType.gpl:
case LicenseType.lgpl:
result = DisallowedLicense._(body, type, origin: origin);
case LicenseType.defaultTemplate:
throw 'should not be creating a LicenseType.defaultTemplate license, it is not a real type';
}
assert(result.type == type);
return result;
}
factory License.fromBodyAndName(String body, String name, { required String origin }) {
body = reformat(body);
LicenseType type = convertLicenseNameToType(name);
if (type == LicenseType.unknown) {
type = convertBodyToType(body);
}
return License.fromBodyAndType(body, type, reformatted: true, origin: origin);
}
factory License.fromBody(String body, { required String origin, bool reformatted = false }) {
if (!reformatted) {
body = reformat(body);
}
final LicenseType type = convertBodyToType(body);
return License.fromBodyAndType(body, type, reformatted: true, origin: origin);
}
factory License.fromCopyrightAndLicense(String copyright, String template, LicenseType type, { required String origin }) {
copyright = reformat(copyright);
template = reformat(template);
return TemplateLicense._(copyright, template, type, origin: origin);
}
factory License.fromIdentifyingReference(String identifyingReference, { required String referencer }) {
String body;
LicenseType type = LicenseType.unknown;
switch (identifyingReference) {
case 'Apache-2.0 OR MIT': // SPDX ID
case 'Apache-2.0': // SPDX ID
case 'Apache:2.0':
case 'http://www.apache.org/licenses/LICENSE-2.0':
case 'https://www.apache.org/licenses/LICENSE-2.0':
// If you're wondering why Abseil has what appears to be a duplicate copy of
// the Apache license, it's because of this:
// https://github.com/abseil/abseil-cpp/pull/270/files#r793181143
body = system.File('data/apache-license-2.0').readAsStringSync();
type = LicenseType.apache;
case 'Apache-2.0 WITH LLVM-exception': // SPDX ID
case 'https://llvm.org/LICENSE.txt':
body = system.File('data/apache-license-2.0-with-llvm-exception').readAsStringSync();
type = LicenseType.llvm;
case 'https://developers.google.com/open-source/licenses/bsd':
body = system.File('data/google-bsd').readAsStringSync();
type = LicenseType.bsd;
case 'http://polymer.github.io/LICENSE.txt':
body = system.File('data/polymer-bsd').readAsStringSync();
type = LicenseType.bsd;
case 'http://www.eclipse.org/legal/epl-v10.html':
body = system.File('data/eclipse-1.0').readAsStringSync();
type = LicenseType.eclipse;
case 'COPYING3:3':
body = system.File('data/gpl-3.0').readAsStringSync();
type = LicenseType.gpl;
case 'COPYING.LIB:2':
case 'COPYING.LIother.m_:2': // blame hyatt
body = system.File('data/library-gpl-2.0').readAsStringSync();
type = LicenseType.lgpl;
case 'GNU Lesser:2':
// there has never been such a license, but the authors said they meant the LGPL2.1
case 'GNU Lesser:2.1':
body = system.File('data/lesser-gpl-2.1').readAsStringSync();
type = LicenseType.lgpl;
case 'COPYING.RUNTIME:3.1':
case 'GCC Runtime Library Exception:3.1':
body = system.File('data/gpl-gcc-exception-3.1').readAsStringSync();
case 'Academic Free License:3.0':
body = system.File('data/academic-3.0').readAsStringSync();
type = LicenseType.afl;
case 'Mozilla Public License:1.1':
body = system.File('data/mozilla-1.1').readAsStringSync();
type = LicenseType.mpl;
case 'http://mozilla.org/MPL/2.0/:2.0':
body = system.File('data/mozilla-2.0').readAsStringSync();
type = LicenseType.mpl;
case 'MIT': // SPDX ID
case 'http://opensource->org/licenses/MIT': // i don't even
case 'http://opensource.org/licenses/MIT':
case 'https://opensource.org/licenses/MIT':
body = system.File('data/mit').readAsStringSync();
type = LicenseType.mit;
case 'Unicode-DFS-2016': // SPDX ID
case 'http://unicode.org/copyright.html#Exhibit1':
case 'http://www.unicode.org/copyright.html#License':
case 'http://www.unicode.org/copyright.html':
case 'http://www.unicode.org/terms_of_use.html': // redirects to copyright.html
case 'https://www.unicode.org/copyright.html':
case 'https://www.unicode.org/terms_of_use.html': // redirects to copyright.html
body = system.File('data/unicode').readAsStringSync();
type = LicenseType.unicode;
case 'http://www.ietf.org/rfc/rfc3454.txt':
body = system.File('data/ietf').readAsStringSync();
type = LicenseType.ietf;
default: throw 'unknown identifyingReference $identifyingReference';
}
return License.fromBodyAndType(body, type, origin: '$identifyingReference referenced by $referencer');
}
License._(String body, this.type, {
required this.origin,
String? authors,
bool yesWeKnowWhatItLooksLikeButItIsNot = false
}) : authors = authors ?? _readAuthors(body) {
assert(() {
try {
switch (type) {
case LicenseType.afl:
case LicenseType.apsl:
case LicenseType.eclipse:
case LicenseType.gpl:
case LicenseType.lgpl:
// We do not want this kind of license in our build.
assert(this is DisallowedLicense);
case LicenseType.apache:
case LicenseType.freetype:
case LicenseType.ijg:
case LicenseType.ietf:
case LicenseType.libpng:
case LicenseType.llvm:
case LicenseType.unicode:
case LicenseType.vulkan:
case LicenseType.zlib:
assert(this is MessageLicense);
case LicenseType.apacheNotice:
assert(this is UniqueLicense);
case LicenseType.bison:
assert(this is BlankLicense);
case LicenseType.bsd:
case LicenseType.mit:
assert(this is TemplateLicense);
case LicenseType.icu:
case LicenseType.openssl:
assert(this is MultiLicense);
case LicenseType.mpl:
assert(this is MozillaLicense);
case LicenseType.unknown:
assert(this is MessageLicense || this is UniqueLicense);
case LicenseType.defaultTemplate:
assert(false, 'should not be creating LicenseType.defaultTemplate license');
}
} on AssertionError {
throw 'incorrectly created a $runtimeType for a $type';
}
return true;
}());
final LicenseType detectedType = convertBodyToType(body);
if (detectedType != LicenseType.unknown && detectedType != type && !yesWeKnowWhatItLooksLikeButItIsNot) {
throw 'Created a license of type $type but it looks like $detectedType:\n---------\n$body\n-----------';
}
if (type != LicenseType.apache && type != LicenseType.llvm && type != LicenseType.vulkan && type != LicenseType.apacheNotice && body.contains('Apache')) {
throw 'Non-Apache license (type=$type, detectedType=$detectedType) contains the word "Apache"; maybe it\'s a notice?:\n---\n$body\n---';
}
if (detectedType != LicenseType.unknown && detectedType != type && !yesWeKnowWhatItLooksLikeButItIsNot) {
throw 'Created a license of type $type but it looks like $detectedType.';
}
if (body.contains(trailingColon)) {
throw 'incomplete license detected:\n---\n$body\n---';
}
bool isUTF8 = true;
late List<int> latin1Encoded;
try {
latin1Encoded = latin1.encode(body);
isUTF8 = false;
} on ArgumentError { /* Fall through to next encoding check. */ }
if (!isUTF8) {
bool isAscii = false;
try {
ascii.decode(latin1Encoded);
isAscii = true;
} on FormatException { /* Fall through to next encoding check */ }
if (isAscii) {
return;
}
try {
utf8.decode(latin1Encoded);
isUTF8 = true;
} on FormatException { /* We check isUTF8 below and throw if necessary */ }
if (isUTF8) {
throw 'tried to create a License object with text that appears to have been misdecoded as Latin1 instead of as UTF-8:\n$body';
}
}
}
final String? authors;
final String origin;
final LicenseType type;
Assignment assignLicenses(String target, LicenseSource source) {
return Assignment(this, target, source);
}
// This takes a second license, which has been pre-split into copyright and
// licenseBody, and uses this license to expand it into a new license. How
// this works depends on this license; for example BSD licenses typically take
// their body and put on the given copyright, (and the licenseBody argument
// here in those cases is usually a reference to that license); some other
// licenses turn into two, one for the original license and one for this
// copyright/body pair.
Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin });
String toStringBody(LicenseSource source);
static final RegExp _copyrightForAuthors = RegExp(
r'Copyright [-0-9 ,(cC)©]+\b(The .+ Authors)\.',
caseSensitive: false
);
static String? _readAuthors(String body) {
final List<Match> matches = _copyrightForAuthors.allMatches(body).toList();
if (matches.isEmpty) {
return null;
}
if (matches.length > 1) {
throw 'found too many authors for this copyright:\n$body\n\n${StackTrace.current}\n\n';
}
return matches[0].group(1);
}
@override
String toString() => '$runtimeType ($type) from $origin';
}
class _LineRange {
_LineRange(this.start, this.end, this._body);
final int start;
final int end;
final String _body;
String? _value;
String get value {
_value ??= _body.substring(start, end);
return _value!;
}
}
Iterable<_LineRange> _walkLinesBackwards(String body, int start) sync* {
int? end;
while (start > 0) {
start -= 1;
if (body[start] == '\n') {
if (end != null) {
yield _LineRange(start + 1, end, body);
}
end = start;
}
}
if (end != null) {
yield _LineRange(start, end, body);
}
}
Iterable<_LineRange> _walkLinesForwards(String body, { int start = 0, int? end }) sync* {
int? startIndex = start == 0 || body[start-1] == '\n' ? start : null;
int endIndex = startIndex ?? start;
end ??= body.length;
while (endIndex < end) {
if (body[endIndex] == '\n') {
if (startIndex != null) {
yield _LineRange(startIndex, endIndex, body);
}
startIndex = endIndex + 1;
}
endIndex += 1;
}
if (startIndex != null) {
yield _LineRange(startIndex, endIndex, body);
}
}
class _SplitLicense {
_SplitLicense(this._body, this._split) : assert(_split == 0 || _split == _body.length || _body[_split] == '\n');
final String _body;
final int _split;
String getCopyright() => _body.substring(0, _split);
String getConditions() => _split >= _body.length ? '' : _body.substring(_split == 0 ? 0 : _split + 1);
}
_SplitLicense _splitLicense(String body, { bool verifyResults = true }) {
final Iterator<_LineRange> lines = _walkLinesForwards(body).iterator;
if (!lines.moveNext()) {
throw 'tried to split empty license';
}
int end = 0;
String endReason;
while (true) {
final String line = lines.current.value;
if (line == 'Author:' ||
line == 'This code is derived from software contributed to Berkeley by' ||
line == 'The Initial Developer of the Original Code is') {
if (!lines.moveNext()) {
throw 'unexpected end of block instead of author when looking for copyright';
}
if (lines.current.value.trim() == '') {
throw 'unexpectedly blank line instead of author when looking for copyright';
}
end = lines.current.end;
if (!lines.moveNext()) {
endReason = 'ran out of text after author';
break;
}
} else if (line.startsWith('Authors:') || line == 'Other contributors:') {
if (line != 'Authors:') {
// assume this line contained an author as well
end = lines.current.end;
}
if (!lines.moveNext()) {
throw 'unexpected end of license when reading list of authors while looking for copyright';
}
final String firstAuthor = lines.current.value;
int subindex = 0;
while (subindex < firstAuthor.length && (firstAuthor[subindex] == ' ')) {
subindex += 1;
}
if (subindex == 0 || subindex > firstAuthor.length) {
throw 'unexpected blank line instead of authors found when looking for copyright';
}
end = lines.current.end;
final String prefix = firstAuthor.substring(0, subindex);
bool hadMoreLines;
while ((hadMoreLines = lines.moveNext()) && lines.current.value.startsWith(prefix)) {
final String nextAuthor = lines.current.value.substring(prefix.length);
if (nextAuthor == '' || nextAuthor[0] == ' ') {
throw 'unexpectedly ragged author list when looking for copyright';
}
end = lines.current.end;
}
if (!hadMoreLines) {
endReason = 'ran out of text while collecting authors';
break;
}
} else if (line.contains(halfCopyrightPattern)) {
do {
if (!lines.moveNext()) {
throw 'unexpected end of block instead of copyright holder when looking for copyright';
}
if (lines.current.value.trim() == '') {
throw 'unexpectedly blank line instead of copyright holder when looking for copyright';
}
end = lines.current.end;
} while (lines.current.value.contains(trailingComma));
if (!lines.moveNext()) {
endReason = 'ran out of text after matching halfCopyrightPattern/trailingComma sequence';
break;
}
} else if (copyrightStatementPatterns.any(line.contains)) {
end = lines.current.end;
if (!lines.moveNext()) {
endReason = 'ran out of text after copyright statement pattern';
break;
}
} else {
endReason = 'line did not match any copyright patterns ("$line")';
break;
}
}
if (verifyResults && 'Copyright ('.allMatches(body, end).isNotEmpty && !body.startsWith('If you modify libpng')) {
throw 'the license seems to contain a copyright:\n===copyright===\n${body.substring(0, end)}\n===license===\n${body.substring(end)}\n=========\ntermination reason: $endReason';
}
return _SplitLicense(body, end);
}
class _PartialLicenseMatch {
_PartialLicenseMatch(this._body, this.start, this.split, this.end, this._match, { required this.hasCopyrights }) : assert(split >= start),
assert(split == start || _body[split] == '\n');
final String _body;
final int start;
final int split;
final int end;
final Match _match;
String? group(int? index) => _match.group(index!);
String? getAuthors() {
final Match? match = authorPattern.firstMatch(getCopyrights());
if (match != null) {
return match.group(1);
}
return null;
}
String getCopyrights() => _body.substring(start, split);
String getConditions() => _body.substring(split + 1, end);
String getEntireLicense() => _body.substring(start, end);
final bool hasCopyrights;
}
// Look for all matches of `pattern` in `body` and return them along with associated copyrights.
Iterable<_PartialLicenseMatch> _findLicenseBlocks(String body, RegExp pattern, int firstPrefixIndex, int indentPrefixIndex, { bool needsCopyright = true }) sync* {
// I tried doing this with one big RegExp initially, but that way lay madness.
for (final Match match in pattern.allMatches(body)) {
assert(match.groupCount >= firstPrefixIndex);
assert(match.groupCount >= indentPrefixIndex);
int start = match.start;
final String fullPrefix = '${match.group(firstPrefixIndex)}${match.group(indentPrefixIndex)}';
// first we walk back to the start of the block that has the same prefix (e.g.
// the start of this comment block)
int firstLineOffset = 0;
bool lastWasBlank = false;
bool foundNonBlank = false;
for (final _LineRange range in _walkLinesBackwards(body, start)) {
String line = range.value;
bool isBlockCommentLine;
if (line.length > 3 && line.endsWith('*/')) {
int index = line.length - 3;
while (line[index] == ' ') {
index -= 1;
}
line = line.substring(0, index + 1);
isBlockCommentLine = true;
} else {
isBlockCommentLine = false;
}
if (line.isEmpty || fullPrefix.startsWith(line)) {
// this is a blank line
if (lastWasBlank && (foundNonBlank || !needsCopyright)) {
break;
}
lastWasBlank = true;
} else if (!isBlockCommentLine && line.startsWith('/*')) {
start = range.start;
firstLineOffset = 2;
break;
} else if (line.startsWith('<!--')) {
start = range.start;
firstLineOffset = 4;
break;
} else if (fullPrefix.isNotEmpty && !line.startsWith(fullPrefix)) {
break;
} else if (licenseFragments.any((RegExp pattern) => line.contains(pattern))) {
// we're running into another license, abort, abort!
break;
} else {
lastWasBlank = false;
foundNonBlank = true;
}
start = range.start;
}
// then we walk forward dropping anything until the first line that matches what
// we think might be part of a copyright statement
bool foundAny = false;
RegExp? debugFirstPattern;
copyrightSearch: for (final _LineRange range in _walkLinesForwards(body, start: start, end: match.start)) {
String line = range.value;
if (firstLineOffset > 0) {
line = line.substring(firstLineOffset);
firstLineOffset = 0;
} else if (line.startsWith(fullPrefix)) {
line = line.substring(fullPrefix.length);
} else {
assert(line.isEmpty || fullPrefix.startsWith(line), 'invariant violated: expected this to be a blank line but it was "$line" (prefix is "$fullPrefix").');
continue copyrightSearch;
}
for (final RegExp pattern in copyrightStatementLeadingPatterns) {
if (line.contains(pattern)) {
start = range.start;
foundAny = true;
debugFirstPattern = pattern;
break copyrightSearch;
}
}
}
// At this point we have figured out what might be copyright text before the license (if anything).
int split;
if (!foundAny) {
if (needsCopyright) {
throw 'could not find copyright before license\nlicense body was:\n---\n${body.substring(match.start, match.end)}\n---\nfile was:\n---\n$body\n---';
}
start = match.start;
split = match.start;
} else {
final String copyrights = body.substring(start, match.start);
final String undecoratedCopyrights = reformat(copyrights);
// Let's try splitting the copyright block as if it was a license.
// This will tell us if we collected something in the copyright block
// that was more license than copyright and that therefore should be
// examined closer.
final _SplitLicense consistencyCheck = _splitLicense(undecoratedCopyrights, verifyResults: false);
final String conditions = consistencyCheck.getConditions();
if (conditions != '') {
// Copyright lines long enough to spill to a second line can create
// false positives; try to weed those out.
final String resplitCopyright = consistencyCheck.getCopyright();
if (resplitCopyright.trim().contains('\n') ||
conditions.trim().contains('\n') ||
resplitCopyright.length < 70 ||
conditions.length > 15) {
throw 'potential license text caught in _findLicenseBlocks copyright dragnet:\n---\n$conditions\n---\nundecorated copyrights was:\n---\n$undecoratedCopyrights\n---\ncopyrights was:\n---\n$copyrights\n---\nblock was:\n---\n${body.substring(start, match.end)}\n---\nfirst line matched: $debugFirstPattern\n---\npattern:\n$pattern\n---\n${StackTrace.current}\n=============';
}
}
if (!copyrights.contains(anySlightSignOfCopyrights)) {
throw 'could not find copyright before license block:\n---\ncopyrights was:\n---\n$copyrights\n---\nblock was:\n---\n${body.substring(start, match.end)}\n---';
}
assert(body[match.start - 1] == '\n', 'match did not start at a newline; match.start = ${match.start}, match.end = ${match.end}, split at: "${body[match.start - 1]}"');
split = match.start - 1;
}
yield _PartialLicenseMatch(body, start, split, match.end, match, hasCopyrights: foundAny);
}
}
class _LicenseMatch {
_LicenseMatch(this.license, this.start, this.end, {
this.debug = '',
this.ignoreWhenCheckingOverlappingRegions = false,
this.missingCopyrights = false
});
final License license;
final int start;
final int end;
final String debug;
final bool ignoreWhenCheckingOverlappingRegions;
final bool missingCopyrights;
@override
String toString() {
return '$start..$end: $license';
}
}
Iterable<_LicenseMatch> _expand(License template, String copyright, String body, int start, int end, { String debug = '', required String origin }) sync* {
final List<License> results = template._expandTemplate(reformat(copyright), body, origin: origin).toList();
if (results.isEmpty) {
throw 'license could not be expanded';
}
yield _LicenseMatch(results.first, start, end, debug: 'expanding template for $debug');
if (results.length > 1) {
yield* results.skip(1).map((License license) => _LicenseMatch(license, start, end, ignoreWhenCheckingOverlappingRegions: true, debug: 'expanding subsequent template for $debug'));
}
}
Iterable<_LicenseMatch> _tryReferenceByFilename(String body, LicenseFileReferencePattern pattern, LicenseSource parentDirectory, { required String origin }) sync* {
if (pattern.copyrightIndex != null) {
for (final Match match in pattern.pattern.allMatches(body)) {
final String copyright = match.group(pattern.copyrightIndex!)!;
final String? authors = pattern.authorIndex != null ? match.group(pattern.authorIndex!) : null;
final String filename = match.group(pattern.fileIndex)!;
final License? template = parentDirectory.nearestLicenseWithName(filename, authors: authors);
if (template == null) {
throw 'failed to find template $filename in $parentDirectory (authors=$authors)';
}
assert(reformat(copyright) != '');
final String entireLicense = body.substring(match.start, match.end);
yield* _expand(template, copyright, entireLicense, match.start, match.end, debug: '_tryReferenceByFilename (with explicit copyright) looking for $filename', origin: origin);
}
} else {
for (final _PartialLicenseMatch match in _findLicenseBlocks(body, pattern.pattern, pattern.firstPrefixIndex, pattern.indentPrefixIndex, needsCopyright: pattern.needsCopyright)) {
final String? authors = match.getAuthors();
String? filename = match.group(pattern.fileIndex);
if (filename == 'modp_b64.c') {
filename = 'modp_b64.cc'; // it was renamed but other files reference the old name
}
// There's also special cases for fuchsia/sdk/linux/dart/zircon/lib/src/fakes/handle_disposition.dart
// (which points to "The Flutter Authors" instead of "The Fuchsia Authors" for mysterious reasons) and
// third_party/angle/src/common/fuchsia_egl/fuchsia_egl.* (which does something similar), but those
// files never reach here because they're marked as binary files in filesystem.dart.
final License? template = parentDirectory.nearestLicenseWithName(filename!, authors: authors);
if (template == null) {
throw
'failed to find accompanying "$filename" in $parentDirectory\n'
'block:\n---\n${match.getEntireLicense()}\n---';
}
if (match.getCopyrights() == '') {
yield _LicenseMatch(template, match.start, match.end, debug: '_tryReferenceByFilename (with failed copyright search) looking for $filename');
} else {
yield* _expand(template, match.getCopyrights(), match.getEntireLicense(), match.start, match.end, debug: '_tryReferenceByFilename (with successful copyright search) looking for $filename', origin: origin);
}
}
}
}
Iterable<_LicenseMatch> _tryReferenceByType(String body, RegExp pattern, LicenseSource parentDirectory, { required String origin }) sync* {
for (final _PartialLicenseMatch match in _findLicenseBlocks(body, pattern, 1, 2, needsCopyright: false)) {
final LicenseType type = convertLicenseNameToType(match.group(3));
final License? template = parentDirectory.nearestLicenseOfType(type);
if (template == null) {
throw 'failed to find accompanying $type license in $parentDirectory';
}
if (match.getCopyrights() == '') {
yield _LicenseMatch(template, match.start, match.end, debug: '_tryReferenceByType (without copyrights) for type $type');
} else {
yield* _expand(template, match.getCopyrights(), match.getEntireLicense(), match.start, match.end, debug: '_tryReferenceByType (with successful copyright search) for type $type', origin: origin);
}
}
}
License _dereferenceLicense(int groupIndex, String? Function(int index) group, LicenseReferencePattern pattern, LicenseSource parentDirectory, { required String origin }) {
License? result = pattern.checkLocalFirst ? parentDirectory.nearestLicenseWithName(group(groupIndex)!) : null;
result ??= License.fromIdentifyingReference(group(groupIndex)!, referencer: origin);
return result;
}
Iterable<_LicenseMatch> _tryReferenceByIdentifyingReference(String body, LicenseReferencePattern pattern, LicenseSource parentDirectory, { required String origin }) sync* {
for (final _PartialLicenseMatch match in _findLicenseBlocks(body, pattern.pattern!, 1, 2, needsCopyright: false)) {
if (pattern.spdx) {
// Per legal advice, we allowlist the use of SPDX headers. Currently, we
// recognize SPDX headers in code from Khronos. To identify such code, we
// examine the copyright that came with the SPDX header or we look at the
// library name. We also accept the headers in some libcxx files that have
// their own license and that otherwise would trip up our code checking
// for missed licenses and copyrights. To identify those, we use the
// library name and the parent directory name or copyright.
bool allowSpdx = false;
final String copyrights = match.getCopyrights();
if (copyrights.contains('The Khronos Group') ||
parentDirectory.libraryName == 'spirv-cross' ||
(parentDirectory.libraryName == 'libcxx' && parentDirectory.name == 'ryu') ||
(parentDirectory.libraryName == 'libcxx' && copyrights.contains('Microsoft'))) {
allowSpdx = true;
}
if (!allowSpdx) {
// Skip this match.
continue;
}
}
assert(match.group(3) != null, 'pattern ${pattern.pattern!} did not have three groups when matched against:\n---\n$body\n---\nmatch: $match');
final License template = _dereferenceLicense(3, match.group, pattern, parentDirectory, origin: origin);
if (match.getCopyrights() == '') {
yield _LicenseMatch(template, match.start, match.end, debug: '_tryReferenceByIdentifyingReference (without copyrights)');
} else {
yield* _expand(template, match.getCopyrights(), match.getEntireLicense(), match.start, match.end, debug: '_tryReferenceByIdentifyingReference (with copyright)', origin: origin);
}
}
}
Iterable<_LicenseMatch> _tryInline(String body, RegExp pattern, {
required bool needsCopyright,
required String origin,
}) sync* {
for (final _PartialLicenseMatch match in _findLicenseBlocks(body, pattern, 1, 2, needsCopyright: needsCopyright)) {
yield _LicenseMatch(License.fromBody(match.getEntireLicense(), origin: origin), match.start, match.end, debug: '_tryInline', missingCopyrights: needsCopyright && !match.hasCopyrights);
}
}
Iterable<_LicenseMatch> _tryStray(String body, RegExp pattern, LicenseSource parentDirectory, { required String origin }) sync* {
// this one doesn't look for copyrights (that's the point, the patterns _are_ the copyrights)
bool gotTemplate = false;
License? template;
for (final Match match in pattern.allMatches(body)) {
if (!gotTemplate) {
template = parentDirectory.nearestLicenseOfType(LicenseType.defaultTemplate);
gotTemplate = true;
}
if (template != null) {
yield* _expand(template, match.group(0)!, match.group(0)!, match.start, match.end, debug: '_tryStray (with template)', origin: origin);
} else {
yield _LicenseMatch(License.fromBody(match.group(0)!, origin: origin), match.start, match.end, debug: '_tryStray');
}
}
}
Iterable<_LicenseMatch> _tryForwardReferencePattern(String fileContents, ForwardReferencePattern pattern, License template, LicenseSource source, { required String origin }) sync* {
String? body;
for (final _PartialLicenseMatch match in _findLicenseBlocks(fileContents, pattern.pattern, pattern.firstPrefixIndex, pattern.indentPrefixIndex)) {
body ??= template.toStringBody(source);
if (!body.contains(pattern.targetPattern)) {
throw
'forward license reference to unexpected license\n'
'license:\n---\n$body\n---\nexpected pattern:\n---\n${pattern.targetPattern}\n---';
}
yield* _expand(template, match.getCopyrights(), match.getEntireLicense(), match.start, match.end, debug: '_tryForwardReferencePattern', origin: origin);
}
}
List<License> determineLicensesFor(String fileContents, String filename, LicenseSource? parentDirectory, { required String origin }) {
if (parentDirectory == null) {
throw 'Fatal error: determineLicensesFor was called with parentDirectory=null!';
}
if (fileContents.length > kMaxSize) {
fileContents = fileContents.substring(0, kMaxSize);
}
final List<_LicenseMatch> results = <_LicenseMatch>[];
fileContents = stripAsciiArt(fileContents);
results.addAll(csReferencesByFilename.expand((LicenseFileReferencePattern pattern) => _tryReferenceByFilename(fileContents, pattern, parentDirectory, origin: origin)));
results.addAll(csReferencesByType.expand((RegExp pattern) => _tryReferenceByType(fileContents, pattern, parentDirectory, origin: origin)));
results.addAll(csReferencesByIdentifyingReference.expand((LicenseReferencePattern pattern) => _tryReferenceByIdentifyingReference(fileContents, pattern, parentDirectory, origin: origin)));
results.addAll(csTemplateLicenses.expand((RegExp pattern) => _tryInline(fileContents, pattern, needsCopyright: true, origin: origin)));
results.addAll(csNoticeLicenses.expand((RegExp pattern) => _tryInline(fileContents, pattern, needsCopyright: false, origin: origin)));
_LicenseMatch? usedTemplate;
if (results.length == 1) {
final _LicenseMatch target = results.single;
results.addAll(csForwardReferenceLicenses.expand((ForwardReferencePattern pattern) => _tryForwardReferencePattern(fileContents, pattern, target.license, parentDirectory, origin: origin)));
if (results.length > 1) {
usedTemplate = target;
}
}
for (final _LicenseMatch match in results.where((_LicenseMatch match) => match.missingCopyrights)) {
throw 'found a license for $filename but could not match its copyright:\n----8<----\n${match.license}\n----8<----';
}
if (results.isEmpty) {
if ((fileContents.contains(copyrightMentionPattern) && fileContents.contains(licenseMentionPattern)) && !fileContents.contains(copyrightMentionOkPattern)) {
throw 'failed to find any license but saw unmatched potential copyright and license statements; first twenty lines:\n----8<----\n${fileContents.split("\n").take(20).join("\n")}\n----8<----';
}
}
// Some files have the odd copyright that isn't explicitly attached to a
// license; we treat those as notice licenses. Only such copyrights
// allowlisted in csStrayCopyrights are handled this way, though. For each of
// these, we have to make sure they don't overlap any of the actual licenses
// matched earlier, so we check for overlaps on each one first.
strays: for (final _LicenseMatch stray in csStrayCopyrights.expand((RegExp pattern) => _tryStray(fileContents, pattern, parentDirectory, origin: origin))) {
for (final _LicenseMatch full in results) {
if (stray.start >= full.start && stray.end <= full.end) {
continue strays;
}
}
results.add(stray);
}
final List<_LicenseMatch> verificationList = results.toList();
if (usedTemplate != null && !verificationList.contains(usedTemplate)) {
verificationList.add(usedTemplate);
}
verificationList.sort((_LicenseMatch a, _LicenseMatch b) {
final int result = a.start - b.start;
if (result != 0) {
return result;
}
return a.end - b.end;
});
int position = 0;
for (final _LicenseMatch m in verificationList) {
if (m.ignoreWhenCheckingOverlappingRegions) {
continue;
} // some text expanded into multiple licenses, so overlapping is expected
if (position > m.start) {
system.stderr.writeln('\n\noverlapping licenses:');
for (final _LicenseMatch n in results) {
system.stderr.writeln(
'license match: ${n.start}..${n.end}, ${n.license.runtimeType}, ${n.debug}\n'
' first line: ${n.license.toStringBody(parentDirectory).split("\n").first}\n'
' last line: ${n.license.toStringBody(parentDirectory).split("\n").last}'
);
}
throw 'overlapping licenses in $filename (one ends at $position, another starts at ${m.start})';
}
if (position < m.start) {
final String substring = fileContents.substring(position, m.start);
if (substring.contains(copyrightMentionPattern) && !substring.contains(copyrightMentionOkPattern)) {
throw 'there is another unmatched potential copyright statement in $filename:\n $position..${m.start}: "$substring"\nmatched licenses: $results';
}
if (substring.contains(licenseMentionPattern)) {
throw 'there is another unmatched potential license in $filename:\n $position..${m.start}: "$substring"\nmatched licenses: $results';
}
}
position = m.end;
}
final String substring = fileContents.substring(position);
if (substring.contains(copyrightMentionPattern) && !substring.contains(copyrightMentionOkPattern)) {
throw 'there is an unmatched potential copyright statement in $filename:\n $position..end: "$substring"\nmatched licenses: $results';
}
if (substring.contains(licenseMentionPattern)) {
throw 'there is an unmatched potential license in $filename:\n $position..end: "$substring"\nmatched licenses: $results';
}
return results.map((_LicenseMatch entry) => entry.license).toSet().toList();
}
License? interpretAsRedirectLicense(String fileContents, LicenseSource parentDirectory, { required String origin }) {
_SplitLicense split;
try {
split = _splitLicense(fileContents);
} on String {
return null;
}
final String body = split.getConditions().trim();
License? result;
for (final LicenseReferencePattern pattern in csReferencesByIdentifyingReference) {
if (pattern.spdx) {
// We don't support SPDX headers in files that use _RepositoryLicenseRedirectFile.
// Before changing this, obtain legal advice.
continue;
}
final Match? match = pattern.pattern!.matchAsPrefix(body);
if (match != null && match.start == 0 && match.end == body.length) {
final License candidate = _dereferenceLicense(3, match.group as String? Function(int?), pattern, parentDirectory, origin: origin);
if (result != null) {
throw 'Multiple potential matches in interpretAsRedirectLicense in $parentDirectory; body was:\n------8<------\n$fileContents\n------8<------';
}
result = candidate;
}
}
return result;
}
// the kind of license that just wants to show a message (e.g. the JPEG one)
class MessageLicense extends License {
MessageLicense._(this.body, LicenseType type, { required String origin }) : super._(body, type, origin: origin);
final String body;
@override
String toStringBody(LicenseSource source) => body;
@override
Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin }) => <License>[this];
}
// the kind of license that says to include the copyright and the license text (e.g. BSD)
class TemplateLicense extends License {
TemplateLicense._(this.defaultCopyright, this.terms, LicenseType type, { String? authors, required String origin })
: assert(!defaultCopyright.endsWith('\n')),
assert(!terms.startsWith('\n')),
assert(terms.isNotEmpty),
super._('$defaultCopyright\n\n$terms', type, origin: origin, authors: authors);
factory TemplateLicense._autosplit(String body, LicenseType type, { String? authors, required String origin }) {
final _SplitLicense bits = _splitLicense(body);
final String copyright = bits.getCopyright();
final String terms = bits.getConditions();
assert((copyright.isEmpty && terms == body) || ('$copyright\n$terms' == body) || (copyright == body && terms.isEmpty), '_splitLicense contract violation.\nCOPYRIGHT:\n===\n$copyright\n===\nTERMS:\n===\n$terms\n===\nBODY:\n===\n$body\n===\n');
int copyrightLength = copyright.length;
while (copyrightLength > 0 && copyright[copyrightLength - 1] == '\n') {
copyrightLength -= 1;
}
int termsStart = 0;
while (termsStart < terms.length && terms[termsStart] == '\n') {
termsStart += 1;
}
return TemplateLicense._(
copyright.substring(0, copyrightLength),
terms.substring(termsStart),
type,
authors: authors,
origin: origin,
);
}
final String defaultCopyright;
final String terms;
@override
String toStringBody(LicenseSource source) {
if (defaultCopyright.isEmpty) {
return terms;
}
return '$defaultCopyright\n\n$terms';
}
@override
Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin }) {
return <License>[ License.fromCopyrightAndLicense(copyright, terms, type, origin: '$origin + ${this.origin}') ];
}
}
// The kind of license that expands to two license blocks a main license and the referring block
// (e.g. OpenSSL).
//
// This is a lawyer-suggested workaround for handling BSD-style licenses where instead of there
// being a single license block where it's obvious what is meant by "above copyright notice" and
// "this list of conditions", we instead have a bunch of similar licenses, each with their own
// copyright, plus there's a copyright in the file that (probably) doesn't exactly match any of the
// copyrights in the file, plus some text saying that that license applies to the file.
class MultiLicense extends License {
MultiLicense._(this.body, LicenseType type, {
String? authors,
required String origin,
bool yesWeKnowWhatItLooksLikeButItIsNot = false,
}) : super._(body, type, origin: origin, authors: authors, yesWeKnowWhatItLooksLikeButItIsNot: yesWeKnowWhatItLooksLikeButItIsNot);
final String body;
@override
String toStringBody(LicenseSource source) => body;
@override
Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin }) {
// Sometimes a license (e.g. the OpenSSL license in the BoringSSL package) is referenced
// from a file that has its own copyright header. When that happens we just print the referenced
// license and the reference to that license separately because it's not at all clear how we're
// supposed to merge them otherwise.
licenseBody = reformat(licenseBody);
assert(licenseBody.startsWith(copyright), 'copyright:\n---\n$copyright\n---\nlicenseBody:\n---\n$licenseBody\n---');
return <License>[
this,
License.fromBody(licenseBody, origin: '$origin (with ${this.origin})', reformatted: true),
];
}
}
// the kind of license that should not be combined with separate copyright notices
class UniqueLicense extends License {
UniqueLicense._(this.body, LicenseType type, {
required String origin,
String? authors,
bool yesWeKnowWhatItLooksLikeButItIsNot = false
}) : super._(body, type, origin: origin, yesWeKnowWhatItLooksLikeButItIsNot: yesWeKnowWhatItLooksLikeButItIsNot, authors: authors);
final String body;
@override
String toStringBody(LicenseSource source) => body;
@override
Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin }) {
throw 'attempted to expand non-template license with "$copyright"\ntemplate was: $this';
}
}
// the kind of license that doesn't need to be reported anywhere
class BlankLicense extends License {
BlankLicense._(super.body, super.type, { required super.origin }) : super._();
@override
Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin }) {
// We don't care what copyrights this kind of license has, we don't need
// to report them. Just report |this| (which is always blank, see below).
return <License>[this];
}
@override
String toStringBody(LicenseSource source) => '';
}
// MPL
class MozillaLicense extends License {
MozillaLicense._(this.body, LicenseType type, { required String origin }) : assert(type == LicenseType.mpl), super._(body, type, origin: origin);
final String body;
@override
Assignment assignLicenses(String target, LicenseSource source) {
if (source.libraryName != 'fallback_root_certificates') {
throw 'Only fallack_root_certificates is allowed to use the MPL.';
}
return Assignment(this, target, source);
}
@override
Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin }) {
throw 'attempted to expand non-template license with "$copyright"\ntemplate was: $this';
}
@override
String toStringBody(LicenseSource source) {
final StringBuffer result = StringBuffer();
result.writeln(body);
result.writeln();
result.writeln("You may obtain a copy of this library's Source Code Form from: ${source.officialSourceLocation}");
return result.toString();
}
}
class DisallowedLicense extends License {
DisallowedLicense._(this.body, LicenseType type, { required String origin }) : super._(body, type, origin: origin);
final String body;
@override
Assignment assignLicenses(String target, LicenseSource source) {
throw '$target (in ${source.libraryName}) attempted to use $origin which is a disallowed license type ($type)';
}
@override
Iterable<License> _expandTemplate(String copyright, String licenseBody, { required String origin }) {
throw 'attempted to use $origin which is a disallowed license type ($type)';
}
@override
String toStringBody(LicenseSource source) {
throw '${source.libraryName} attempted to use $origin which is a disallowed license type ($type)';
}
}