From ece5e6f051582264f96eca79322f359529345891 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Fri, 15 Aug 2025 20:40:27 -0700 Subject: [PATCH 1/3] feat: fix compatibility with multiple notices --- Cargo.lock | 2 +- Cargo.toml | 8 +- Lisensor.toml | 3 +- README.md | 71 +++++++-- Taskfile.yml | 11 ++ src/cli.rs | 66 +++++++++ src/config.rs | 68 +++------ src/format.rs | 135 +++++++++++++----- src/lib.rs | 17 +++ src/main.rs | 42 ++---- src/runner.rs | 69 +++++---- tests/fixture_test.rs | 131 +++++++++++++++++ tests/fixtures/.gitignore | 1 + tests/fixtures/after_copyright.txt | 4 + tests/fixtures/after_copyright.txt_cfail | 0 tests/fixtures/after_copyright.txt_ffail | 0 tests/fixtures/after_copyright.txt_fixed | 4 + tests/fixtures/before_license.txt | 4 + tests/fixtures/before_license.txt_cfail | 1 + tests/fixtures/before_license.txt_ffail | 0 tests/fixtures/before_license.txt_fixed | 5 + tests/fixtures/empty_text.txt | 0 tests/fixtures/empty_text.txt_cfail | 1 + tests/fixtures/empty_text.txt_ffail | 0 tests/fixtures/empty_text.txt_fixed | 2 + tests/fixtures/middle_license.txt | 4 + tests/fixtures/middle_license.txt_cfail | 1 + tests/fixtures/middle_license.txt_ffail | 0 tests/fixtures/middle_license.txt_fixed | 5 + tests/fixtures/multi_correct.txt | 6 + tests/fixtures/multi_correct.txt_cfail | 0 tests/fixtures/multi_correct.txt_ffail | 0 tests/fixtures/multi_correct.txt_fixed | 6 + tests/fixtures/multi_wrong.txt | 6 + tests/fixtures/multi_wrong.txt_cfail | 1 + tests/fixtures/multi_wrong.txt_ffail | 1 + tests/fixtures/multi_wrong.txt_fixed | 6 + tests/fixtures/only_1line.txt | 2 + tests/fixtures/only_1line.txt_cfail | 1 + tests/fixtures/only_1line.txt_ffail | 0 tests/fixtures/only_1line.txt_fixed | 4 + tests/fixtures/sentinel_first.txt | 7 + tests/fixtures/sentinel_first.txt_cfail | 1 + tests/fixtures/sentinel_first.txt_ffail | 0 tests/fixtures/sentinel_first.txt_fixed | 9 ++ tests/fixtures/wrong_holder.txt | 4 + tests/fixtures/wrong_holder.txt_cfail | 1 + tests/fixtures/wrong_holder.txt_ffail | 0 tests/fixtures/wrong_holder.txt_fixed | 4 + tests/fixtures/wrong_holder_license.txt | 6 + tests/fixtures/wrong_holder_license.txt_cfail | 1 + tests/fixtures/wrong_holder_license.txt_ffail | 0 tests/fixtures/wrong_holder_license.txt_fixed | 6 + tests/fixtures/wrong_license.txt | 3 + tests/fixtures/wrong_license.txt_cfail | 1 + tests/fixtures/wrong_license.txt_ffail | 0 tests/fixtures/wrong_license.txt_fixed | 4 + tests/fixtures/wrong_year.txt | 4 + tests/fixtures/wrong_year.txt_cfail | 1 + tests/fixtures/wrong_year.txt_ffail | 0 tests/fixtures/wrong_year.txt_fixed | 5 + tests/fixtures/wrong_year_future.txt | 4 + tests/fixtures/wrong_year_future.txt_cfail | 1 + tests/fixtures/wrong_year_future.txt_ffail | 1 + tests/fixtures/wrong_year_future.txt_fixed | 4 + tests/fixtures/wrong_year_future_range.txt | 4 + .../wrong_year_future_range.txt_cfail | 1 + tests/fixtures/wrong_year_range.txt | 5 + tests/fixtures/wrong_year_range.txt_cfail | 1 + tests/fixtures/wrong_year_range.txt_ffail | 0 tests/fixtures/wrong_year_range.txt_fixed | 5 + 71 files changed, 622 insertions(+), 149 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/lib.rs create mode 100644 tests/fixture_test.rs create mode 100644 tests/fixtures/.gitignore create mode 100644 tests/fixtures/after_copyright.txt create mode 100644 tests/fixtures/after_copyright.txt_cfail create mode 100644 tests/fixtures/after_copyright.txt_ffail create mode 100644 tests/fixtures/after_copyright.txt_fixed create mode 100644 tests/fixtures/before_license.txt create mode 100644 tests/fixtures/before_license.txt_cfail create mode 100644 tests/fixtures/before_license.txt_ffail create mode 100644 tests/fixtures/before_license.txt_fixed create mode 100644 tests/fixtures/empty_text.txt create mode 100644 tests/fixtures/empty_text.txt_cfail create mode 100644 tests/fixtures/empty_text.txt_ffail create mode 100644 tests/fixtures/empty_text.txt_fixed create mode 100644 tests/fixtures/middle_license.txt create mode 100644 tests/fixtures/middle_license.txt_cfail create mode 100644 tests/fixtures/middle_license.txt_ffail create mode 100644 tests/fixtures/middle_license.txt_fixed create mode 100644 tests/fixtures/multi_correct.txt create mode 100644 tests/fixtures/multi_correct.txt_cfail create mode 100644 tests/fixtures/multi_correct.txt_ffail create mode 100644 tests/fixtures/multi_correct.txt_fixed create mode 100644 tests/fixtures/multi_wrong.txt create mode 100644 tests/fixtures/multi_wrong.txt_cfail create mode 100644 tests/fixtures/multi_wrong.txt_ffail create mode 100644 tests/fixtures/multi_wrong.txt_fixed create mode 100644 tests/fixtures/only_1line.txt create mode 100644 tests/fixtures/only_1line.txt_cfail create mode 100644 tests/fixtures/only_1line.txt_ffail create mode 100644 tests/fixtures/only_1line.txt_fixed create mode 100644 tests/fixtures/sentinel_first.txt create mode 100644 tests/fixtures/sentinel_first.txt_cfail create mode 100644 tests/fixtures/sentinel_first.txt_ffail create mode 100644 tests/fixtures/sentinel_first.txt_fixed create mode 100644 tests/fixtures/wrong_holder.txt create mode 100644 tests/fixtures/wrong_holder.txt_cfail create mode 100644 tests/fixtures/wrong_holder.txt_ffail create mode 100644 tests/fixtures/wrong_holder.txt_fixed create mode 100644 tests/fixtures/wrong_holder_license.txt create mode 100644 tests/fixtures/wrong_holder_license.txt_cfail create mode 100644 tests/fixtures/wrong_holder_license.txt_ffail create mode 100644 tests/fixtures/wrong_holder_license.txt_fixed create mode 100644 tests/fixtures/wrong_license.txt create mode 100644 tests/fixtures/wrong_license.txt_cfail create mode 100644 tests/fixtures/wrong_license.txt_ffail create mode 100644 tests/fixtures/wrong_license.txt_fixed create mode 100644 tests/fixtures/wrong_year.txt create mode 100644 tests/fixtures/wrong_year.txt_cfail create mode 100644 tests/fixtures/wrong_year.txt_ffail create mode 100644 tests/fixtures/wrong_year.txt_fixed create mode 100644 tests/fixtures/wrong_year_future.txt create mode 100644 tests/fixtures/wrong_year_future.txt_cfail create mode 100644 tests/fixtures/wrong_year_future.txt_ffail create mode 100644 tests/fixtures/wrong_year_future.txt_fixed create mode 100644 tests/fixtures/wrong_year_future_range.txt create mode 100644 tests/fixtures/wrong_year_future_range.txt_cfail create mode 100644 tests/fixtures/wrong_year_range.txt create mode 100644 tests/fixtures/wrong_year_range.txt_cfail create mode 100644 tests/fixtures/wrong_year_range.txt_ffail create mode 100644 tests/fixtures/wrong_year_range.txt_fixed diff --git a/Cargo.lock b/Cargo.lock index cdeec47..c83f283 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -383,7 +383,7 @@ checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "lisensor" -version = "0.1.0" +version = "0.2.0" dependencies = [ "chrono", "pistonite-cu", diff --git a/Cargo.toml b/Cargo.toml index a41af72..83612e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lisensor" -version = "0.1.0" +version = "0.2.0" edition = "2024" description = "Tool to automatically add, check, and fix license notices in the source files" license = "MIT" @@ -22,9 +22,13 @@ serde = "1" [dependencies.cu] package = "pistonite-cu" version = "0.4.0" -features = ["cli", "fs", "coroutine-heavy", "toml"] +features = ["print", "fs", "coroutine-heavy", "toml"] # path = "../cu/packages/copper" +[features] +default = ["cli"] +cli = ["cu/cli"] + [package.metadata.binstall.signing] algorithm = "minisign" pubkey = "RWThJQKJaXayoZBe0YV5LV4KFkQwcqQ6Fg9dJBz18JnpHGdf/cHUyKs+" diff --git a/Lisensor.toml b/Lisensor.toml index b0c636b..40417cc 100644 --- a/Lisensor.toml +++ b/Lisensor.toml @@ -1,2 +1,3 @@ [Pistonite] -"src/**/*.rs" = "MIT" +"src/**/*" = "MIT" +"tests/**/*.rs" = "MIT" diff --git a/README.md b/README.md index e8a18e2..772a648 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,13 @@ ![License Badge](https://img.shields.io/github/license/Pistonite/lisensor) ![Issue Badge](https://img.shields.io/github/issues/Pistonite/lisensor) +``` +# install from source +cargo install lisensor +# install prebuilt binary with cargo-binstall +cargo binstall lisensor +``` + Lisensor (pronounced *licenser*) is a tool that automatically adds a license notice to your source file, like this: @@ -19,6 +26,8 @@ For languages such as python, the comment style will automatically be changed to `#` instead of `//`. Languages that do not have either of the comment styles are currently not supported. (Feel free to PR). +## Usage + The CLI usage is: ``` lisensor [CONFIG] ... [-f] @@ -27,12 +36,16 @@ lisensor [CONFIG] ... [-f] `CONFIG` is one of more config file for Lisensor (see below). `-f` will attempt to automatically fix the files in place. +The crate also has a library target of the same name. It's intended to be +used by tests in the project, but might be helpful for integrating `lisensor` into your own tooling. + ## Config By default, `lisensor` looks for `Lisensor.toml` then `lisensor.toml` in the current directory if no config files are specified. -Paths in the config file are relative to the directory containing -the config file, meaning running `lisensor` from anywhere will resulting -in the same outcome. + +Globs in the config file are relative to the directory containing +the config file, meaning running `lisensor` from anywhere will result +in the same outcome. Currently, symlinks are not followed. The config file should contain one table per copyright holder. The table should contain key-value pairs, where the keys are @@ -42,7 +55,7 @@ and the value is any SPDX ID, but the value is not validated. For example: ```toml -[Pistonite] +["Foobar contributors"] "**/*.rs" = "MIT" ``` @@ -76,10 +89,48 @@ if a file is covered by conflicting configs. This prevents "fake" successful fixes. However, the fix mode might still edit the file according to one of the configs specified (arbitrarily chosen) before reporting the error. -## Line Ending -When checking, any line ending is accepted. When fixing, it will turn the file -into UNIX line ending. +## Format Behavior Details +The tool will check and ensure the following conditions are true for a file: + +1. The first line is the license line, with the comment style for that file, + and starting with `SPDX-License-Identifier: ` after the comment, followed + by the expected license identifier. +2. The second line is the copy right line, with the comment style for that file, + and starting with `Copyright (c) `, follow by a year range, where the start + can be anything and the end must be the current year at the local time the tool + is ran. If the start and end are the same year, then a single year is sufficient. + The year range is followed by a space, then the copyright holder. +3. No other line(s) exist that matches the same `SPDX-License-Identifier` + or `Copyright (c)` format for license and copyright lines, respectively. + +The tool will not attempt fixing the file, if any copyright line is found +with the wrong holder. This ensures that the tool never accidentally override +license notices from the original source file. + +If a source file contains license notice(s) from its original authors, +you must specify a *sentinel* line after your license notice. The tool +will skip checking all contents after the sentinel line. The sentinel line +is the same comment style followed by `* * * * *` (five `*` separated by four spaces). +Anything can follow after that. For example: + +``` +// SPDX-License-Identifier: MIT +// Copyright (c) 2024-2025 Foobar contributors +// * * * * * other license information below +// SPDX-License-Identifier: MIT +// Copyright (c) 2017-2018 Bizbaz contributors +``` + +When the year becomes `2026`, the tool will change the notice +for `Foobar contributors`, but will not touch anything below the sentinel line. + +Other: -When checking, only the license and copyright lines are checked. -When fixing, it will add an empty line between the notice -and the rest of the file, if there isn't already. +- Line ending: + - When checking, any line ending is accepted. When fixing, it will turn the file + into UNIX line ending. + - When checking, only the first 2 lines are checked, the rest of the file + is ignored. + - When fixing, if the third line is not a sentinel line or empty line, + it will ensure there's an empty line between the license notice and the + rest of the content. diff --git a/Taskfile.yml b/Taskfile.yml index 73b984d..28ccb7b 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -11,6 +11,9 @@ tasks: - rm -rf mono-dev - git clone https://github.com/Pistonight/mono-dev --depth 1 + build-lib: + - cargo build --no-default-features + check: - task: cargo:clippy-all - task: cargo:fmt-check @@ -20,6 +23,9 @@ tasks: - cargo test - task: test-inline-cmd + test-fixt: + - cargo test --test fixture_test + test-inline-cmd: dir: src cmds: @@ -29,3 +35,8 @@ tasks: - cargo run -- -f - task: cargo:fmt-fix - task: check + + clean: + - cargo clean + - rm -f tests/fixtures/*.txt_out + - rustup update diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..35c75de --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Pistonite + +use cu::pre::*; + +use crate::Config; + +/// Check or fix license notices +#[derive(Debug, Clone, PartialEq, clap::Parser)] +pub struct Cli { + /// Attempt fix the license notice on the files + #[clap(short, long)] + pub fix: bool, + /// In inline config mode, specify the copyright holder + #[clap(short = 'H', long, requires("license"))] + pub holder: Option, + /// In inline config mode, specify the SPDX ID for the license + #[clap(short = 'L', long, requires("holder"))] + pub license: Option, + + #[clap(flatten)] + pub common: cu::cli::Flags, + + /// Paths to config files, or in inline config mode, glob patterns for source files + /// to apply the license notice. + pub paths: Vec, +} + +/// Convert the CLI args into configuration object +pub fn config_from_cli(args: &mut crate::Cli) -> cu::Result { + match (args.holder.take(), args.license.take()) { + (Some(holder), Some(license)) => { + if let Some(config_path) = crate::try_find_default_config_file() { + cu::bail!( + "--holder or --license cannot be specified when {config_path} is present in the current directory" + ); + } + Ok(Config::new( + holder, + license, + std::mem::take(&mut args.paths), + )) + } + // clap ensures both are None + _ => { + let mut iter = std::mem::take(&mut args.paths).into_iter(); + let mut config = match iter.next() { + None => { + let Some(config_path) = crate::try_find_default_config_file() else { + cu::bail!( + "cannot find Lisensor.toml, and no config files are specified on the command line." + ); + }; + Config::build(config_path)? + } + Some(first) => Config::build(&first)?, + }; + + for path in iter { + config.absorb(Config::build(&path)?)?; + } + + Ok(config) + } + } +} diff --git a/src/config.rs b/src/config.rs index ebf1a53..62b2e8c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,64 +8,31 @@ use std::sync::Arc; use cu::pre::*; -/// Convert the CLI args into configuration object -pub fn load_config(args: &mut crate::Cli) -> cu::Result { - match (args.holder.take(), args.license.take()) { - (Some(holder), Some(license)) => { - if let Some(config_path) = find_config_path() { - cu::bail!( - "--holder or --license cannot be specified when {config_path} is present in the current directory" - ); - } - Ok(Config::inline( - holder, - license, - std::mem::take(&mut args.paths), - )) - } - // clap ensures both are None - _ => { - let mut iter = std::mem::take(&mut args.paths).into_iter(); - let mut config = match iter.next() { - None => { - let Some(config_path) = find_config_path() else { - cu::bail!( - "cannot find Lisensor.toml, and no config files are specified on the command line." - ); - }; - Config::build(config_path)? - } - Some(first) => Config::build(&first)?, - }; - - for path in iter { - config.absorb(Config::build(&path)?)?; - } - - Ok(config) - } - } -} - -fn find_config_path() -> Option<&'static str> { +/// Try finding the default config files according to the order +/// specified in the documentation (see repo README) +pub fn try_find_default_config_file() -> Option<&'static str> { ["Lisensor.toml", "lisensor.toml"] .into_iter() .find(|x| Path::new(x).exists()) } +/// Config object +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct Config { + // glob -> (holder, license) + globs: BTreeMap, Arc)>, +} + /// Raw config read from a toml config file. /// /// The format is holder -> glob -> license #[derive(Deserialize)] struct TomlConfig(BTreeMap>); -/// Config object -pub struct Config { - // glob -> (holder, license) - globs: BTreeMap, Arc)>, -} impl Config { - pub fn inline(holder: String, license: String, glob_list: Vec) -> Self { + /// Create a config object from a single holder and license, + /// with multiple glob patterns. + pub fn new(holder: String, license: String, glob_list: Vec) -> Self { let holder = Arc::new(holder); let license = Arc::new(license); let mut globs = BTreeMap::new(); @@ -83,7 +50,11 @@ impl Config { } Self { globs } } - /// Build the config from a path, error if conflicts are detected + + /// Build the config by reading the file specified, error if conflicts are detected + /// + /// The globs specified in the config file are relative to the parent directory + /// of `path`. pub fn build(path: &str) -> cu::Result { let raw = toml::parse::(&cu::fs::read_string(path)?)?; let parent = Path::new(path) @@ -155,7 +126,10 @@ impl Config { impl Config { /// Iterate the resolve paths as (path, holder, license) + #[allow(clippy::should_implement_trait)] pub fn into_iter(self) -> impl Iterator, Arc)> { + // we can't implement the IntoIterator trait because + // the map object has an unnamed function type self.globs .into_iter() .map(|(path, (holder, license))| (path, holder, license)) diff --git a/src/format.rs b/src/format.rs index bb18c16..5ebb642 100644 --- a/src/format.rs +++ b/src/format.rs @@ -7,7 +7,7 @@ use std::sync::LazyLock; use cu::pre::*; -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Format { /// The `// ...` format SlashSlash, @@ -41,6 +41,24 @@ impl Format { } } + /// Strip the copyright line if it's the right format. + /// Return "YYYY[-YYYY] HOLDER" + pub fn check_strip_copyright_line(self, line: &str) -> Option<&str> { + match self { + Self::SlashSlash => line.strip_prefix("// Copyright (c) "), + Self::Hash => line.strip_prefix("# Copyright (c) "), + } + } + + /// Check if the line starts with sentinel comment + pub fn starts_with_sentinel(self, line: &str) -> bool { + match self { + Self::SlashSlash => line.starts_with("// * * * * *"), + Self::Hash => line.starts_with("# * * * * *"), + } + } + + /// Format the license notice into a buffer pub fn format( self, year_start: u32, @@ -80,15 +98,6 @@ impl Format { } Ok(()) } - - /// Strip the copyright line if it's the right format. - /// Return "YYYY[-YYYY] HOLDER" - pub fn check_strip_copyright_line(self, line: &str) -> Option<&str> { - match self { - Self::SlashSlash => line.strip_prefix("// Copyright (c) "), - Self::Hash => line.strip_prefix("# Copyright (c) "), - } - } } pub fn check_file(path: &Path, expected_holder: &str, expected_license: &str) -> cu::Result<()> { @@ -109,7 +118,7 @@ pub fn check_file(path: &Path, expected_holder: &str, expected_license: &str) -> let line = cu::check!(line, "error while reading file '{}'", path.display())?; let copyright_info = format.check_strip_copyright_line(&line); - let copyright_info = cu::check!(copyright_info, "missing copyright line at the top.")?; + let copyright_info = cu::check!(copyright_info, "missing copyright line.")?; let (_, year_end, actual_holder) = parse_copyright_info(copyright_info); if actual_holder != expected_holder { @@ -127,51 +136,103 @@ pub fn fix_file(path: &Path, expected_holder: &str, expected_license: &str) -> c let format = Format::from_path(path); let reader = cu::fs::reader(path)?; let lines = reader.lines(); - let mut buf1 = String::new(); - let mut buf2 = String::new(); + let mut buf = FixBuf::default(); + let mut found_license_line = false; let mut found_copyright_line = false; + let mut found_sentinel = false; for line in lines { let line = cu::check!(line, "error while reading file '{}'", path.display())?; + if found_sentinel { + buf.push_line(&line, format); + continue; + } + if format.starts_with_sentinel(&line) { + found_sentinel = true; + buf.push_line(&line, format); + continue; + } if format.check_strip_license_line(&line).is_some() { if found_license_line { - cu::bail!("duplicate license lines found, not auto-fixable, please fix manually"); + cu::bail!( + "multiple license line found! Consider adding a sentinel line if there are other license notices that need to be kept!" + ); } found_license_line = true; continue; } if let Some(copyright_info) = format.check_strip_copyright_line(&line) { if found_copyright_line { - cu::bail!("duplicate copyright lines found, not auto-fixable, please fix manually"); + cu::bail!( + "multiple copyright line found! Consider adding a sentinel line if there are other license notices that need to be kept!" + ); } found_copyright_line = true; - let year_start = parse_copyright_info(copyright_info).0; - format.format(year_start, expected_holder, expected_license, &mut buf1)?; - if !buf2.starts_with('\n') { - buf1.push('\n'); + let (year_start, _, _) = parse_copyright_info(copyright_info); + if year_start > current_year() { + cu::bail!("copyright start year is in the future! Manual fix required."); } - buf1.push_str(&buf2); + buf.perform_fix_if_need(format, year_start, expected_holder, expected_license)?; continue; } - if buf1.is_empty() { - buf2.push_str(&line); - buf2.push('\n'); + buf.push_line(&line, format); + } + // format new notice if didn't find one + buf.perform_fix_if_need(format, current_year(), expected_holder, expected_license)?; + + cu::fs::write(path, buf.buf)?; + Ok(()) +} + +#[derive(Default)] +struct FixBuf { + buf: String, + fixed: bool, + fixed_when_empty: bool, +} +impl FixBuf { + fn push_line(&mut self, line: &str, format: Format) { + if self.fixed_when_empty { + if !format.starts_with_sentinel(line) && !line.is_empty() { + self.buf.reserve(line.len() + 2); + self.buf.push('\n'); + } else { + self.buf.reserve(line.len() + 1); + } + self.fixed_when_empty = false; } else { - buf1.push_str(&line); - buf1.push('\n'); + self.buf.reserve(line.len() + 1); } + + self.buf.push_str(line); + self.buf.push('\n'); } - if buf1.is_empty() { - format.format(current_year(), expected_holder, expected_license, &mut buf1)?; - if !buf2.starts_with('\n') { - buf1.push('\n'); + fn perform_fix_if_need( + &mut self, + format: Format, + year_start: u32, + holder: &str, + license: &str, + ) -> cu::Result<()> { + if self.fixed { + return Ok(()); } - buf1.push_str(&buf2); + let current_content = std::mem::take(&mut self.buf); + format.format(year_start, holder, license, &mut self.buf)?; + // add an empty line if needed + if !current_content.is_empty() { + if !format.starts_with_sentinel(¤t_content) && !current_content.starts_with('\n') + { + self.buf.push('\n'); + } + } else { + self.fixed_when_empty = true; + } + self.buf.push_str(¤t_content); + self.fixed = true; + Ok(()) } - - cu::fs::write(path, buf1)?; - Ok(()) } fn parse_copyright_info(info: &str) -> (u32, u32, &str) { @@ -191,11 +252,15 @@ fn parse_copyright_info(info: &str) -> (u32, u32, &str) { (year_start, year_end.max(year_start)) } }; - let owner = parts.next().unwrap_or(""); - (year_start, year_end, owner) + let holder = parts.next().unwrap_or(""); + (year_start, year_end, holder) } fn current_year() -> u32 { + if cfg!(test) { + // mock current year in tests + return 2025; + } static YEAR: LazyLock = LazyLock::new(|| { use chrono::Datelike; let y = chrono::Local::now().year().max(0) as u32; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2fff7be --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Pistonite + +//! See README on crates.io or GitHub. + +mod config; +pub use config::*; + +mod runner; +pub use runner::*; +mod format; +pub use format::*; + +#[cfg(feature = "cli")] +mod cli; +#[cfg(feature = "cli")] +pub use cli::*; diff --git a/src/main.rs b/src/main.rs index d25059b..6a8a87e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,36 +1,20 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2025 Pistonite -use cu::pre::*; - -mod config; -use config::*; -mod format; -mod runner; - -/// Check or fix license notices -#[derive(clap::Parser, Clone)] -struct Cli { - /// Attempt fix the license notice on the files - #[clap(short, long)] - fix: bool, - /// In inline config mode, specify the copyright holder - #[clap(short = 'H', long, requires("license"))] - holder: Option, - /// In inline config mode, specify the SPDX ID for the license - #[clap(short = 'L', long, requires("holder"))] - license: Option, - - #[clap(flatten)] - common: cu::cli::Flags, - - /// Paths to config files, or in inline config mode, glob patterns for source files - /// to apply the license notice. - paths: Vec, -} +use lisensor::{Cli, config_from_cli, run}; #[cu::cli(flags = "common")] async fn main(mut args: Cli) -> cu::Result<()> { - let config = load_config(&mut args)?; - runner::run(config.into_iter(), args.fix).await + let fix = args.fix; + let result = run(config_from_cli(&mut args)?, fix).await?; + + if result.is_err() { + if fix { + cu::bail!("some issues could not be fixed automatically."); + } else { + cu::bail!("license check unsuccesful."); + } + } + + Ok(()) } diff --git a/src/runner.rs b/src/runner.rs index 727bdb5..d12fb59 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -5,12 +5,30 @@ use std::collections::BTreeMap; use std::path::PathBuf; use std::sync::Arc; -use crate::format; +use crate::{Config, format}; -pub async fn run(iter: I, fix: bool) -> cu::Result<()> -where - I: IntoIterator, Arc)>, -{ +/// Issues found +#[derive(Debug, Default, Clone, PartialEq)] +pub struct Failure { + pub errors: Vec, +} + +impl std::fmt::Display for Failure { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for e in &self.errors { + e.fmt(f)?; + writeln!(f)?; + } + Ok(()) + } +} + +/// Run the tool for the given config. +/// +/// - `Ok(Ok(())` means successful. +/// - `Ok(Err(failure))` means the run was successful, but issues are found. +/// - `Err(e)` means the run itself was not successful. +pub async fn run(config: Config, fix: bool) -> cu::Result> { let bar = cu::progress_unbounded_lowp(if fix { "fixing files" } else { @@ -24,7 +42,7 @@ where // avoid opening too many files. max open 1024 files let pool = cu::co::pool(1024); - for (glob, holder, license) in iter.into_iter() { + for (glob, holder, license) in config.into_iter() { let result = run_glob( &glob, holder, @@ -64,30 +82,30 @@ where } let mut count = 0; - let mut failed = 0; + let mut errors = vec![]; while let Some(result) = set.next().await { // join error - let (path, ok) = result?; + let (path, result) = result?; // handle check error - if !ok { - failed += 1; + if let Err(e) = result { + errors.push(e) } count += 1; cu::progress!(&bar, count, "{}", path.display()); } - if failed != 0 { + if !errors.is_empty() { + let failed = errors.len(); cu::error!("checked {total} files, found {failed} issue(s)."); cu::hint!("run with --fix to fix them automatically."); - if fix { - cu::bail!("some issues could not be fixed automatically."); - } else { - cu::bail!("license check unsuccesful."); - } + + let errors = errors.into_iter().map(|x| x.to_string()).collect(); + + return Ok(Err(Failure { errors })); } cu::info!("license check successful for {total} files."); - Ok(()) + Ok(Ok(())) } fn run_glob( @@ -96,13 +114,16 @@ fn run_glob( license: Arc, fix: bool, pool: &cu::co::Pool, - handles: &mut Vec>, + handles: &mut Vec)>>, path_map: &mut BTreeMap, Arc)>, ) -> cu::Result { let mut matched = false; for path in cu::fs::glob(glob)? { - matched = true; let path = path?; + if !path.is_file() { + continue; + } + matched = true; let holder = Arc::clone(&holder); let license = Arc::clone(&license); @@ -138,23 +159,23 @@ fn run_glob( pool.spawn(async move { let check_result = format::check_file(&path, &holder, &license); let Err(e) = check_result else { - return (path, true); + return (path, Ok(())); }; cu::trace!("'{}': {e}", path.display()); cu::debug!("fixing '{}'", path.display()); let Err(e) = format::fix_file(&path, &holder, &license) else { - return (path, true); + return (path, Ok(())); }; cu::error!("failed to fix '{}': {e}", path.display()); - (path, false) + (path, Err(e)) }) } else { pool.spawn(async move { let Err(e) = format::check_file(&path, &holder, &license) else { - return (path, true); + return (path, Ok(())); }; cu::warn!("'{}': {e}", path.display()); - (path, false) + (path, Err(e)) }) }; diff --git a/tests/fixture_test.rs b/tests/fixture_test.rs new file mode 100644 index 0000000..d03231e --- /dev/null +++ b/tests/fixture_test.rs @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Pistonite + +use std::path::Path; + +use lisensor::{Config, run}; + +pub fn run_fixture(name: &str) -> cu::Result<()> { + cu::init_print_options(cu::lv::Color::Never, cu::lv::Print::QuietQuiet, None); + let update_output = std::env::var("FIXTURE_UPDATE").unwrap_or_default().as_str() == "1"; + + let fixtures = Path::new("tests").join("fixtures"); + let input_path = fixtures.join(name); + + cu::debug!("running fixture: {name}"); + + let input_copy_path = fixtures.join(format!("{name}_out")); + std::fs::copy(&input_path, &input_copy_path)?; + + let config = Config::new( + "TestHolder".to_string(), + "TestLicense".to_string(), + vec![input_copy_path.to_string_lossy().into_owned()], + ); + let config2 = config.clone(); + + let check_result = cu::co::run(async move { run(config, false).await })?; + let expected_failure = fixtures.join(format!("{name}_cfail")); + if expected_failure.exists() { + let expected_error = cu::fs::read_string(&expected_failure)?; + match check_result { + Ok(_) => { + if !expected_error.trim().is_empty() { + cu::bail!("fixture {name} is supposed to fail in check mode, but it passed."); + } + } + Err(e) => { + let actual_error = e.to_string(); + if actual_error != expected_error { + if update_output { + cu::fs::write(expected_failure, actual_error)?; + } else { + cu::bail!( + "fixture '{name}' check error mismatch.\nexpected={expected_error}\nactual={actual_error}" + ); + } + } + } + } + } else { + if let Err(e) = check_result { + cu::fs::write(expected_failure, e.to_string())?; + } else { + cu::fs::write(expected_failure, "")?; + } + } + + let fix_result = cu::co::run(async move { run(config2, true).await })?; + + let expected_failure = fixtures.join(format!("{name}_ffail")); + if expected_failure.exists() { + let expected_error = cu::fs::read_string(&expected_failure)?; + match fix_result { + Ok(_) => { + if !expected_error.trim().is_empty() { + cu::bail!("fixture {name} is supposed to fail in fix mode, but it passed."); + } + } + Err(e) => { + let actual_error = e.to_string(); + if actual_error != expected_error { + if update_output { + cu::fs::write(expected_failure, actual_error)?; + } else { + cu::bail!( + "fixture '{name}' fix error mismatch.\nexpected={expected_error}\nactual={actual_error}" + ); + } + } + } + } + } else { + if let Err(e) = fix_result { + cu::fs::write(expected_failure, e.to_string())?; + } else { + cu::fs::write(expected_failure, "")?; + } + } + + let expected_output = fixtures.join(format!("{name}_fixed")); + if expected_output.exists() { + let expected_output_content = cu::fs::read_string(&expected_output)?; + let actual_output_content = cu::fs::read_string(input_copy_path)?; + if expected_output_content != actual_output_content { + if update_output { + cu::fs::write(expected_output, actual_output_content)?; + } else { + cu::bail!("fixture '{name}' output mismatch. actual:\n{actual_output_content}"); + } + } + } else { + std::fs::copy(input_copy_path, expected_output)?; + } + + Ok(()) +} + +macro_rules! run_fixture { + ($name:ident) => { + #[test] + fn $name() -> cu::Result<()> { + run_fixture(concat!(stringify!($name), ".txt")) + } + }; +} + +run_fixture!(empty_text); +run_fixture!(only_1line); +run_fixture!(wrong_license); +run_fixture!(wrong_holder); +run_fixture!(wrong_holder_license); +run_fixture!(after_copyright); +run_fixture!(before_license); +run_fixture!(middle_license); +run_fixture!(multi_correct); +run_fixture!(multi_wrong); +run_fixture!(sentinel_first); +run_fixture!(wrong_year); +run_fixture!(wrong_year_future); +run_fixture!(wrong_year_future_range); +run_fixture!(wrong_year_range); diff --git a/tests/fixtures/.gitignore b/tests/fixtures/.gitignore new file mode 100644 index 0000000..f9ba3d8 --- /dev/null +++ b/tests/fixtures/.gitignore @@ -0,0 +1 @@ +*.txt_out diff --git a/tests/fixtures/after_copyright.txt b/tests/fixtures/after_copyright.txt new file mode 100644 index 0000000..147dfe5 --- /dev/null +++ b/tests/fixtures/after_copyright.txt @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder +fn main() { +} diff --git a/tests/fixtures/after_copyright.txt_cfail b/tests/fixtures/after_copyright.txt_cfail new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/after_copyright.txt_ffail b/tests/fixtures/after_copyright.txt_ffail new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/after_copyright.txt_fixed b/tests/fixtures/after_copyright.txt_fixed new file mode 100644 index 0000000..147dfe5 --- /dev/null +++ b/tests/fixtures/after_copyright.txt_fixed @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder +fn main() { +} diff --git a/tests/fixtures/before_license.txt b/tests/fixtures/before_license.txt new file mode 100644 index 0000000..61408f4 --- /dev/null +++ b/tests/fixtures/before_license.txt @@ -0,0 +1,4 @@ +fn main() { +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder +} diff --git a/tests/fixtures/before_license.txt_cfail b/tests/fixtures/before_license.txt_cfail new file mode 100644 index 0000000..3185959 --- /dev/null +++ b/tests/fixtures/before_license.txt_cfail @@ -0,0 +1 @@ +missing license notice line. diff --git a/tests/fixtures/before_license.txt_ffail b/tests/fixtures/before_license.txt_ffail new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/before_license.txt_fixed b/tests/fixtures/before_license.txt_fixed new file mode 100644 index 0000000..064f570 --- /dev/null +++ b/tests/fixtures/before_license.txt_fixed @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder + +fn main() { +} diff --git a/tests/fixtures/empty_text.txt b/tests/fixtures/empty_text.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/empty_text.txt_cfail b/tests/fixtures/empty_text.txt_cfail new file mode 100644 index 0000000..3185959 --- /dev/null +++ b/tests/fixtures/empty_text.txt_cfail @@ -0,0 +1 @@ +missing license notice line. diff --git a/tests/fixtures/empty_text.txt_ffail b/tests/fixtures/empty_text.txt_ffail new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/empty_text.txt_fixed b/tests/fixtures/empty_text.txt_fixed new file mode 100644 index 0000000..add53cf --- /dev/null +++ b/tests/fixtures/empty_text.txt_fixed @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder diff --git a/tests/fixtures/middle_license.txt b/tests/fixtures/middle_license.txt new file mode 100644 index 0000000..d1cf7cb --- /dev/null +++ b/tests/fixtures/middle_license.txt @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: TestLicense +fn main() { +// Copyright (c) 2025 TestHolder +} diff --git a/tests/fixtures/middle_license.txt_cfail b/tests/fixtures/middle_license.txt_cfail new file mode 100644 index 0000000..394231b --- /dev/null +++ b/tests/fixtures/middle_license.txt_cfail @@ -0,0 +1 @@ +missing copyright line. diff --git a/tests/fixtures/middle_license.txt_ffail b/tests/fixtures/middle_license.txt_ffail new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/middle_license.txt_fixed b/tests/fixtures/middle_license.txt_fixed new file mode 100644 index 0000000..064f570 --- /dev/null +++ b/tests/fixtures/middle_license.txt_fixed @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder + +fn main() { +} diff --git a/tests/fixtures/multi_correct.txt b/tests/fixtures/multi_correct.txt new file mode 100644 index 0000000..839f6c3 --- /dev/null +++ b/tests/fixtures/multi_correct.txt @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder +fn main() { +} diff --git a/tests/fixtures/multi_correct.txt_cfail b/tests/fixtures/multi_correct.txt_cfail new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/multi_correct.txt_ffail b/tests/fixtures/multi_correct.txt_ffail new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/multi_correct.txt_fixed b/tests/fixtures/multi_correct.txt_fixed new file mode 100644 index 0000000..839f6c3 --- /dev/null +++ b/tests/fixtures/multi_correct.txt_fixed @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder +fn main() { +} diff --git a/tests/fixtures/multi_wrong.txt b/tests/fixtures/multi_wrong.txt new file mode 100644 index 0000000..ac9f262 --- /dev/null +++ b/tests/fixtures/multi_wrong.txt @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: TestLicense +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder +// Copyright (c) 2025 TestHolder +fn main() { +} diff --git a/tests/fixtures/multi_wrong.txt_cfail b/tests/fixtures/multi_wrong.txt_cfail new file mode 100644 index 0000000..394231b --- /dev/null +++ b/tests/fixtures/multi_wrong.txt_cfail @@ -0,0 +1 @@ +missing copyright line. diff --git a/tests/fixtures/multi_wrong.txt_ffail b/tests/fixtures/multi_wrong.txt_ffail new file mode 100644 index 0000000..5953ccf --- /dev/null +++ b/tests/fixtures/multi_wrong.txt_ffail @@ -0,0 +1 @@ +multiple license line found! Consider adding a sentinel line if there are other license notices that need to be kept! diff --git a/tests/fixtures/multi_wrong.txt_fixed b/tests/fixtures/multi_wrong.txt_fixed new file mode 100644 index 0000000..ac9f262 --- /dev/null +++ b/tests/fixtures/multi_wrong.txt_fixed @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: TestLicense +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder +// Copyright (c) 2025 TestHolder +fn main() { +} diff --git a/tests/fixtures/only_1line.txt b/tests/fixtures/only_1line.txt new file mode 100644 index 0000000..217fff6 --- /dev/null +++ b/tests/fixtures/only_1line.txt @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: TestLicense +fn main() {} diff --git a/tests/fixtures/only_1line.txt_cfail b/tests/fixtures/only_1line.txt_cfail new file mode 100644 index 0000000..394231b --- /dev/null +++ b/tests/fixtures/only_1line.txt_cfail @@ -0,0 +1 @@ +missing copyright line. diff --git a/tests/fixtures/only_1line.txt_ffail b/tests/fixtures/only_1line.txt_ffail new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/only_1line.txt_fixed b/tests/fixtures/only_1line.txt_fixed new file mode 100644 index 0000000..d383379 --- /dev/null +++ b/tests/fixtures/only_1line.txt_fixed @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder + +fn main() {} diff --git a/tests/fixtures/sentinel_first.txt b/tests/fixtures/sentinel_first.txt new file mode 100644 index 0000000..3e24e0a --- /dev/null +++ b/tests/fixtures/sentinel_first.txt @@ -0,0 +1,7 @@ +// * * * * * sentinel is first line +// SPDX-License-Identifier: TestLicense +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2020 TestHolder +// Copyright (c) 2020 TestHolder +fn main() { +} diff --git a/tests/fixtures/sentinel_first.txt_cfail b/tests/fixtures/sentinel_first.txt_cfail new file mode 100644 index 0000000..3185959 --- /dev/null +++ b/tests/fixtures/sentinel_first.txt_cfail @@ -0,0 +1 @@ +missing license notice line. diff --git a/tests/fixtures/sentinel_first.txt_ffail b/tests/fixtures/sentinel_first.txt_ffail new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/sentinel_first.txt_fixed b/tests/fixtures/sentinel_first.txt_fixed new file mode 100644 index 0000000..61ac3bc --- /dev/null +++ b/tests/fixtures/sentinel_first.txt_fixed @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder +// * * * * * sentinel is first line +// SPDX-License-Identifier: TestLicense +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2020 TestHolder +// Copyright (c) 2020 TestHolder +fn main() { +} diff --git a/tests/fixtures/wrong_holder.txt b/tests/fixtures/wrong_holder.txt new file mode 100644 index 0000000..1e120bb --- /dev/null +++ b/tests/fixtures/wrong_holder.txt @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 NotTestHolder +// * * * * * +fn main() {} diff --git a/tests/fixtures/wrong_holder.txt_cfail b/tests/fixtures/wrong_holder.txt_cfail new file mode 100644 index 0000000..eae69e6 --- /dev/null +++ b/tests/fixtures/wrong_holder.txt_cfail @@ -0,0 +1 @@ +holder is wrong: expected 'TestHolder', found 'NotTestHolder'. diff --git a/tests/fixtures/wrong_holder.txt_ffail b/tests/fixtures/wrong_holder.txt_ffail new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/wrong_holder.txt_fixed b/tests/fixtures/wrong_holder.txt_fixed new file mode 100644 index 0000000..49eb60a --- /dev/null +++ b/tests/fixtures/wrong_holder.txt_fixed @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder +// * * * * * +fn main() {} diff --git a/tests/fixtures/wrong_holder_license.txt b/tests/fixtures/wrong_holder_license.txt new file mode 100644 index 0000000..f2e397e --- /dev/null +++ b/tests/fixtures/wrong_holder_license.txt @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: NotTestLicense +// Copyright (c) 2025 NotTestHolder +// * * * * * +// SPDX-License-Identifier: NotTestLicense +// Copyright (c) 2025 NotTestHolder +fn main() {} diff --git a/tests/fixtures/wrong_holder_license.txt_cfail b/tests/fixtures/wrong_holder_license.txt_cfail new file mode 100644 index 0000000..3f8aae4 --- /dev/null +++ b/tests/fixtures/wrong_holder_license.txt_cfail @@ -0,0 +1 @@ +license is wrong: expected 'TestLicense', found 'NotTestLicense'. diff --git a/tests/fixtures/wrong_holder_license.txt_ffail b/tests/fixtures/wrong_holder_license.txt_ffail new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/wrong_holder_license.txt_fixed b/tests/fixtures/wrong_holder_license.txt_fixed new file mode 100644 index 0000000..fb1e0e2 --- /dev/null +++ b/tests/fixtures/wrong_holder_license.txt_fixed @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder +// * * * * * +// SPDX-License-Identifier: NotTestLicense +// Copyright (c) 2025 NotTestHolder +fn main() {} diff --git a/tests/fixtures/wrong_license.txt b/tests/fixtures/wrong_license.txt new file mode 100644 index 0000000..935a67a --- /dev/null +++ b/tests/fixtures/wrong_license.txt @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: NotTestLicense +// Copyright (c) 2025 TestHolder +fn main() {} diff --git a/tests/fixtures/wrong_license.txt_cfail b/tests/fixtures/wrong_license.txt_cfail new file mode 100644 index 0000000..3f8aae4 --- /dev/null +++ b/tests/fixtures/wrong_license.txt_cfail @@ -0,0 +1 @@ +license is wrong: expected 'TestLicense', found 'NotTestLicense'. diff --git a/tests/fixtures/wrong_license.txt_ffail b/tests/fixtures/wrong_license.txt_ffail new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/wrong_license.txt_fixed b/tests/fixtures/wrong_license.txt_fixed new file mode 100644 index 0000000..d383379 --- /dev/null +++ b/tests/fixtures/wrong_license.txt_fixed @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder + +fn main() {} diff --git a/tests/fixtures/wrong_year.txt b/tests/fixtures/wrong_year.txt new file mode 100644 index 0000000..b1378b2 --- /dev/null +++ b/tests/fixtures/wrong_year.txt @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2017 TestHolder +fn main() { +} diff --git a/tests/fixtures/wrong_year.txt_cfail b/tests/fixtures/wrong_year.txt_cfail new file mode 100644 index 0000000..74c1608 --- /dev/null +++ b/tests/fixtures/wrong_year.txt_cfail @@ -0,0 +1 @@ +copyright info ends at 2017, but we are in 2025. diff --git a/tests/fixtures/wrong_year.txt_ffail b/tests/fixtures/wrong_year.txt_ffail new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/wrong_year.txt_fixed b/tests/fixtures/wrong_year.txt_fixed new file mode 100644 index 0000000..90db9c5 --- /dev/null +++ b/tests/fixtures/wrong_year.txt_fixed @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2017-2025 TestHolder + +fn main() { +} diff --git a/tests/fixtures/wrong_year_future.txt b/tests/fixtures/wrong_year_future.txt new file mode 100644 index 0000000..700e4d8 --- /dev/null +++ b/tests/fixtures/wrong_year_future.txt @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2050 TestHolder +fn main() { +} diff --git a/tests/fixtures/wrong_year_future.txt_cfail b/tests/fixtures/wrong_year_future.txt_cfail new file mode 100644 index 0000000..f8cc54b --- /dev/null +++ b/tests/fixtures/wrong_year_future.txt_cfail @@ -0,0 +1 @@ +copyright info ends at 2050, but we are in 2025. diff --git a/tests/fixtures/wrong_year_future.txt_ffail b/tests/fixtures/wrong_year_future.txt_ffail new file mode 100644 index 0000000..ac843f2 --- /dev/null +++ b/tests/fixtures/wrong_year_future.txt_ffail @@ -0,0 +1 @@ +copyright start year is in the future! diff --git a/tests/fixtures/wrong_year_future.txt_fixed b/tests/fixtures/wrong_year_future.txt_fixed new file mode 100644 index 0000000..700e4d8 --- /dev/null +++ b/tests/fixtures/wrong_year_future.txt_fixed @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2050 TestHolder +fn main() { +} diff --git a/tests/fixtures/wrong_year_future_range.txt b/tests/fixtures/wrong_year_future_range.txt new file mode 100644 index 0000000..eb060d7 --- /dev/null +++ b/tests/fixtures/wrong_year_future_range.txt @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2050-2070 TestHolder +fn main() { +} diff --git a/tests/fixtures/wrong_year_future_range.txt_cfail b/tests/fixtures/wrong_year_future_range.txt_cfail new file mode 100644 index 0000000..4a255e0 --- /dev/null +++ b/tests/fixtures/wrong_year_future_range.txt_cfail @@ -0,0 +1 @@ +copyright info ends at 2070, but we are in 2025. diff --git a/tests/fixtures/wrong_year_range.txt b/tests/fixtures/wrong_year_range.txt new file mode 100644 index 0000000..61b65fa --- /dev/null +++ b/tests/fixtures/wrong_year_range.txt @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2010-2016 TestHolder + +fn main() { +} diff --git a/tests/fixtures/wrong_year_range.txt_cfail b/tests/fixtures/wrong_year_range.txt_cfail new file mode 100644 index 0000000..a7c649c --- /dev/null +++ b/tests/fixtures/wrong_year_range.txt_cfail @@ -0,0 +1 @@ +copyright info ends at 2016, but we are in 2025. diff --git a/tests/fixtures/wrong_year_range.txt_ffail b/tests/fixtures/wrong_year_range.txt_ffail new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/wrong_year_range.txt_fixed b/tests/fixtures/wrong_year_range.txt_fixed new file mode 100644 index 0000000..af07910 --- /dev/null +++ b/tests/fixtures/wrong_year_range.txt_fixed @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2010-2025 TestHolder + +fn main() { +} From e8a7f067c5372ff184dd5e3eab00c15219de22cc Mon Sep 17 00:00:00 2001 From: Pistonight Date: Fri, 15 Aug 2025 20:47:13 -0700 Subject: [PATCH 2/3] add note about other license notices --- README.md | 26 +++++++++++++++++++ tests/fixture_test.rs | 1 + tests/fixtures/first_not_sentinel.txt | 6 +++++ tests/fixtures/first_not_sentinel.txt_cfail | 1 + tests/fixtures/first_not_sentinel.txt_ffail | 0 tests/fixtures/first_not_sentinel.txt_fixed | 8 ++++++ tests/fixtures/wrong_year_future.txt_ffail | 2 +- .../wrong_year_future_range.txt_ffail | 1 + .../wrong_year_future_range.txt_fixed | 4 +++ 9 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/first_not_sentinel.txt create mode 100644 tests/fixtures/first_not_sentinel.txt_cfail create mode 100644 tests/fixtures/first_not_sentinel.txt_ffail create mode 100644 tests/fixtures/first_not_sentinel.txt_fixed create mode 100644 tests/fixtures/wrong_year_future_range.txt_ffail create mode 100644 tests/fixtures/wrong_year_future_range.txt_fixed diff --git a/README.md b/README.md index 772a648..1e98b51 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,32 @@ if a file is covered by conflicting configs. This prevents "fake" successful fixes. However, the fix mode might still edit the file according to one of the configs specified (arbitrarily chosen) before reporting the error. +## Compatibility with Other License Notices +It's common if some file is taken from another project, you must include +a license notice if it's not already in the file. In this case, +it's recommended to use a sentinel line (see Format Behavior Details below) +to prevent the tool from accidentally overriding the original notice. + +``` +// See original license notices below: +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Other people +// +// ^ This is bad because running the tool will now +// change the holder and license into you, even though what you want +// is to add another notice on top of it +``` + +Adding a sentinel line will fix this: +``` +// See original license notices below: +// * * * * * +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Other people +// +// ^ Now the tool will ignore everything after * * * * * +``` + ## Format Behavior Details The tool will check and ensure the following conditions are true for a file: diff --git a/tests/fixture_test.rs b/tests/fixture_test.rs index d03231e..2a51e01 100644 --- a/tests/fixture_test.rs +++ b/tests/fixture_test.rs @@ -129,3 +129,4 @@ run_fixture!(wrong_year); run_fixture!(wrong_year_future); run_fixture!(wrong_year_future_range); run_fixture!(wrong_year_range); +run_fixture!(first_not_sentinel); diff --git a/tests/fixtures/first_not_sentinel.txt b/tests/fixtures/first_not_sentinel.txt new file mode 100644 index 0000000..9cf2d3b --- /dev/null +++ b/tests/fixtures/first_not_sentinel.txt @@ -0,0 +1,6 @@ +// * * * * * +// This file was taken from Foobar project. +// Original license notices: +// +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Other people diff --git a/tests/fixtures/first_not_sentinel.txt_cfail b/tests/fixtures/first_not_sentinel.txt_cfail new file mode 100644 index 0000000..3185959 --- /dev/null +++ b/tests/fixtures/first_not_sentinel.txt_cfail @@ -0,0 +1 @@ +missing license notice line. diff --git a/tests/fixtures/first_not_sentinel.txt_ffail b/tests/fixtures/first_not_sentinel.txt_ffail new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/first_not_sentinel.txt_fixed b/tests/fixtures/first_not_sentinel.txt_fixed new file mode 100644 index 0000000..ea8ad26 --- /dev/null +++ b/tests/fixtures/first_not_sentinel.txt_fixed @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2025 TestHolder +// * * * * * +// This file was taken from Foobar project. +// Original license notices: +// +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Other people diff --git a/tests/fixtures/wrong_year_future.txt_ffail b/tests/fixtures/wrong_year_future.txt_ffail index ac843f2..e63f20b 100644 --- a/tests/fixtures/wrong_year_future.txt_ffail +++ b/tests/fixtures/wrong_year_future.txt_ffail @@ -1 +1 @@ -copyright start year is in the future! +copyright start year is in the future! Manual fix required. diff --git a/tests/fixtures/wrong_year_future_range.txt_ffail b/tests/fixtures/wrong_year_future_range.txt_ffail new file mode 100644 index 0000000..e63f20b --- /dev/null +++ b/tests/fixtures/wrong_year_future_range.txt_ffail @@ -0,0 +1 @@ +copyright start year is in the future! Manual fix required. diff --git a/tests/fixtures/wrong_year_future_range.txt_fixed b/tests/fixtures/wrong_year_future_range.txt_fixed new file mode 100644 index 0000000..eb060d7 --- /dev/null +++ b/tests/fixtures/wrong_year_future_range.txt_fixed @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: TestLicense +// Copyright (c) 2050-2070 TestHolder +fn main() { +} From a269293d350859fd3ab6529d1d0ef74b28197229 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Fri, 15 Aug 2025 20:49:41 -0700 Subject: [PATCH 3/3] example in readme --- README.md | 46 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1e98b51..8d15336 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,8 @@ it's recommended to use a sentinel line (see Format Behavior Details below) to prevent the tool from accidentally overriding the original notice. ``` -// See original license notices below: +// This file was taken from Foobar project, see original license notices below +// // SPDX-License-Identifier: MIT // Copyright (c) 2025 Other people // @@ -105,10 +106,37 @@ to prevent the tool from accidentally overriding the original notice. // is to add another notice on top of it ``` +Running the tool will change it to: +``` +// SPDX-License-Identifier: Your License +// Copyright (c) 2025 You + +// This file was taken from Foobar project, see original license notices below +// +// +// ^ This is bad because running the tool will now +// change the holder and license into you, even though what you want +// is to add another notice on top of it +``` + Adding a sentinel line will fix this: ``` -// See original license notices below: // * * * * * +// This file was taken from Foobar project, see original license notices below +// +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 Other people +// +// ^ Now the tool will ignore everything after * * * * * +``` + +Now a new notice will be added +``` +// SPDX-License-Identifier: Your License +// Copyright (c) 2025 You +// * * * * * +// This file was taken from Foobar project, see original license notices below +// // SPDX-License-Identifier: MIT // Copyright (c) 2025 Other people // @@ -153,10 +181,10 @@ for `Foobar contributors`, but will not touch anything below the sentinel line. Other: - Line ending: - - When checking, any line ending is accepted. When fixing, it will turn the file - into UNIX line ending. - - When checking, only the first 2 lines are checked, the rest of the file - is ignored. - - When fixing, if the third line is not a sentinel line or empty line, - it will ensure there's an empty line between the license notice and the - rest of the content. + - When checking, any line ending is accepted. + - When fixing, it will turn the file into UNIX line ending. +- When checking, only the first 2 lines are checked, the rest of the file + is ignored. +- When fixing, if the third line is not a sentinel line or empty line, + it will ensure there's an empty line between the license notice and the + rest of the content.