Repository: dart-lang/pana (dart)

Issue:
License recognition fails on SSH-OpenSSH
The `SSH-OpenSSH` license is not matched with itself, instead it is matched with both `SSH-short` and `SSH-OpenSSH` as potential hit, and then dropping both from the results.

Pull request description:
- Fixes #1484.
- The `findLongestUnclaimedTokenRange` is used to filter out detection results where the unclaimed/unrecognized range is too long (50+ tokens), however, with trailing contained ranges the calculation is wrong.
- Related to #1488, as the contained range is a smaller version of the license. In such cases we should also remove the smaller version, but in that case the edge case bug here wouldn't be noticeable.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d75d27e08..781addae6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 - Fixed small ambiguity in the order of license match evaluation.
 - Resolve `analysis_options.yaml` dev dependency to expose transitive formatter options.
 - Fixed license coverage calculation.
+- Fixed license match post-filtering.
 
 ## 0.22.23
 
diff --git a/lib/src/license_detection/license_detector.dart b/lib/src/license_detection/license_detector.dart
index 8925e84fe..cc6897a25 100644
--- a/lib/src/license_detection/license_detector.dart
+++ b/lib/src/license_detection/license_detector.dart
@@ -268,25 +268,33 @@ double calculateUnclaimedTokenPercentage(
 /// Returns the number of tokens in the longest unclaimed token
 /// sequence.
 int findLongestUnclaimedTokenRange(List<LicenseMatch> matches, int end) {
-  var ranges = <Range>[];
   if (matches.isEmpty) return end;
 
-  for (var match in matches) {
-    ranges.add(Range(match.tokenRange.start, match.tokenRange.end));
-  }
-
-  ranges.sort(sortRangeOnStartValue);
-  var maxTokenSequence = ranges.first.start - 0;
+  var maxUnclaimed = 0;
+
+  // create an end-of-range marker at the end of ranges for easier computation
+  final ranges = [...matches.map((m) => m.tokenRange), Range(end, end)];
+
+  // move position from the start up to the end, checking uncovered ranges
+  for (var pos = 0; pos < end;) {
+    // check if inside a range
+    final skipToEnd = ranges
+        .where((r) => r.containsIndex(pos))
+        .map((r) => r.end)
+        .fold(-1, max);
+    if (skipToEnd > -1) {
+      assert(pos < skipToEnd);
+      pos = skipToEnd;
+      continue;
+    }
 
-  for (var i = 1; i < ranges.length; i++) {
-    maxTokenSequence = max(
-      ranges[i].start - ranges[i - 1].end,
-      maxTokenSequence,
-    );
+    // jump to next range
+    final nextRange = ranges
+        .where((r) => r.start > pos)
+        .reduce((a, b) => a.start.compareTo(b.start) <= 0 ? a : b);
+    maxUnclaimed = max(maxUnclaimed, nextRange.start - pos);
+    pos = nextRange.end;
   }
 
-  maxTokenSequence = max(maxTokenSequence, end - ranges.last.end);
-  return maxTokenSequence;
+  return maxUnclaimed;
 }
-
-int sortRangeOnStartValue(Range a, Range b) => a.start - b.start;
diff --git a/lib/src/license_detection/token_matcher.dart b/lib/src/license_detection/token_matcher.dart
index 0b8dd8822..d5fd5907b 100644
--- a/lib/src/license_detection/token_matcher.dart
+++ b/lib/src/license_detection/token_matcher.dart
@@ -54,6 +54,8 @@ class Range {
         (end >= other.start && end <= other.end);
   }
 
+  bool containsIndex(int value) => start <= value && value < end;
+
   @override
   String toString() => 'Range($start-$end)';
 }


Test patch:
diff --git a/test/license_detector_test.dart b/test/license_detector_test.dart
index d582ac972..29f29077d 100644
--- a/test/license_detector_test.dart
+++ b/test/license_detector_test.dart
@@ -258,6 +258,16 @@ void main() {
       160,
       1000,
     );
+
+    _testLongestUnclaimedTokenRange(
+      'Trailing contained range',
+      [
+        _dummyLicenseMatchInstance(0.9, '', start: 30, end: 940),
+        _dummyLicenseMatchInstance(0.9, '', start: 50, end: 70),
+      ],
+      60,
+      1000,
+    );
   });
 }
 
diff --git a/test/licenses/license_coverage_test.dart b/test/licenses/license_coverage_test.dart
index fdd9507fe..339056df0 100644
--- a/test/licenses/license_coverage_test.dart
+++ b/test/licenses/license_coverage_test.dart
@@ -16,10 +16,6 @@ void main() {
           license.content,
           relativePath: 'LICENSE',
         );
-        // TODO: fix detection and return at least one match (https://github.com/dart-lang/pana/issues/1484)
-        if (license.identifier == 'SSH-OpenSSH') {
-          continue;
-        }
         final match = detected
             .where((l) => l.spdxIdentifier == license.identifier)
             .single;