Skip to content

Commit 49c11fe

Browse files
mstykowclaude
andauthored
fix(copyright): detect open-ended year ranges ending in "now" (#1222)
A copyright line whose year range ends in "now" (e.g. "Copyright (c) 2013-NOW Jinzhu <wosmvp@gmail.com>") was dropped entirely: the "2013-NOW" token failed every year pattern and tagged as a common noun, so the grammar never built a copyright tree. Extend the existing year-dash-present POS pattern (derived from ScanCode's _YEAR_DASH_PRESENT) to also accept "now", matched case-insensitively since it is commonly uppercase in the wild. The match stays anchored to a year in the same token, so standalone "now" and unrelated attached tokens like "2013-final" are not treated as years. Refs aboutcode-org/scancode-toolkit#5220 Signed-off-by: Maxim Stykow <maxim.stykow@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8fa9874 commit 49c11fe

3 files changed

Lines changed: 34 additions & 1 deletion

File tree

src/copyright/detector/tests.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,28 @@ fn test_standalone_c_holder_year_range_with_trailing_period_is_extracted() {
8181
);
8282
}
8383

84+
#[test]
85+
fn test_open_ended_now_year_range_is_extracted() {
86+
// scancode-toolkit#5220: an open-ended year range ending in "NOW" must not
87+
// drop the entire copyright statement.
88+
let input = "Copyright (c) 2013-NOW Jinzhu <wosmvp@gmail.com>";
89+
90+
let (copyrights, holders, _authors) = detect_copyrights_from_text(input);
91+
92+
assert!(
93+
copyrights
94+
.iter()
95+
.any(|c| c.copyright == "Copyright (c) 2013-NOW Jinzhu <wosmvp@gmail.com>"),
96+
"copyrights: {:?}",
97+
copyrights.iter().map(|c| &c.copyright).collect::<Vec<_>>()
98+
);
99+
assert!(
100+
holders.iter().any(|h| h.holder == "Jinzhu"),
101+
"holders: {:?}",
102+
holders.iter().map(|h| &h.holder).collect::<Vec<_>>()
103+
);
104+
}
105+
84106
#[test]
85107
fn test_obfuscated_angle_email_is_kept_in_copyright() {
86108
let input = "(C)opyright MMIV-MMV Anselm R. Garbe <garbeam at gmail dot com>";

src/copyright/patterns.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ fn build_pattern_list() -> Vec<(String, PosTag)> {
9393
let year_short_punct = &format!("{}{}", year_short, punct);
9494
let year_or_year_year = &format!("({}|{})", year_punct, year_year_punct);
9595
let year_then_short = &format!("({}({})*)", year_or_year_year, year_short_punct);
96-
let year_dash_present = &format!(r"{}[\-~]? ?[Pp]resent\.?,?", year);
96+
// Provenant extension: also accept "now" as an open-ended range end, e.g.
97+
// "2013-NOW". ScanCode only handles "present" here; "now" is commonly
98+
// uppercase in the wild, so match it case-insensitively.
99+
let year_dash_present = &format!(r"{}[\-~]? ?([Pp]resent|(?i:now))\.?,?", year);
97100

98101
let mut patterns: Vec<(String, PosTag)> = Vec::with_capacity(1200);
99102

src/copyright/patterns_test.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,14 @@ fn test_year_ranges() {
195195
assert_eq!(p.match_token("2010-202x"), PosTag::Yr);
196196
assert_eq!(p.match_token("1999,2000"), PosTag::Yr);
197197
assert_eq!(p.match_token("2020-present"), PosTag::Yr);
198+
// Open-ended range ending in "now" (scancode-toolkit#5220), including the
199+
// common uppercase form.
200+
assert_eq!(p.match_token("2013-NOW"), PosTag::Yr);
201+
assert_eq!(p.match_token("2013-now"), PosTag::Yr);
202+
assert_eq!(p.match_token("2013now"), PosTag::Yr);
203+
// A year attached to an unrelated word must NOT become a year range.
204+
assert_ne!(p.match_token("2013-final"), PosTag::Yr);
205+
assert_ne!(p.match_token("2020-Jinzhu"), PosTag::Yr);
198206
}
199207

200208
#[test]

0 commit comments

Comments
 (0)