| 1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
| 2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 |
| 3 | |
| 4 | // cSpell: ignore datetime dotdot gettext |
| 5 | |
| 6 | use anyhow::anyhow; |
| 7 | use anyhow::Context; |
| 8 | use anyhow::Result; |
| 9 | use lazy_static::lazy_static; |
| 10 | use std::cell::RefCell; |
| 11 | use std::str::FromStr; |
| 12 | use std::{path::Path, path::PathBuf}; |
| 13 | |
| 14 | #[derive (Copy, Clone, Debug)] |
| 15 | struct LicenseTagStyle { |
| 16 | tag_start: &'static str, |
| 17 | line_prefix: &'static str, |
| 18 | line_indentation: &'static str, |
| 19 | line_break: &'static str, |
| 20 | tag_end: &'static str, |
| 21 | overall_start: &'static str, |
| 22 | overall_end: &'static str, |
| 23 | is_real_end: bool, |
| 24 | } |
| 25 | |
| 26 | impl LicenseTagStyle { |
| 27 | fn c_style_comment_style() -> Self { |
| 28 | Self { |
| 29 | tag_start: "// Copyright © " , |
| 30 | line_prefix: "//" , |
| 31 | line_indentation: " " , |
| 32 | line_break: " \n" , |
| 33 | tag_end: const_format::concatcp!("// " , SPDX_LICENSE_LINE), |
| 34 | overall_start: "" , |
| 35 | overall_end: " \n" , |
| 36 | is_real_end: false, |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | fn shell_comment_style() -> Self { |
| 41 | Self { |
| 42 | tag_start: "# Copyright © " , |
| 43 | line_prefix: "#" , |
| 44 | line_indentation: " " , |
| 45 | line_break: " \n" , |
| 46 | tag_end: const_format::concatcp!("# " , SPDX_LICENSE_LINE), |
| 47 | overall_start: "" , |
| 48 | overall_end: " \n" , |
| 49 | is_real_end: false, |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | fn rst_comment_style() -> Self { |
| 54 | Self { |
| 55 | tag_start: ".. Copyright © " , |
| 56 | line_prefix: ".." , |
| 57 | line_indentation: " " , |
| 58 | line_break: " \n" , |
| 59 | tag_end: const_format::concatcp!(".. " , SPDX_LICENSE_LINE), |
| 60 | overall_start: "" , |
| 61 | overall_end: " \n" , |
| 62 | is_real_end: false, |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | fn html_comment_style() -> Self { |
| 67 | Self { |
| 68 | tag_start: "<!-- Copyright © " , |
| 69 | line_prefix: " " , |
| 70 | line_indentation: "" , |
| 71 | line_break: " ;" , |
| 72 | tag_end: " -->" , |
| 73 | overall_start: "<!--" , |
| 74 | overall_end: " --> \n" , |
| 75 | is_real_end: true, |
| 76 | } |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | struct SourceFileWithTags<'a> { |
| 81 | source: &'a str, |
| 82 | tag_style: &'a LicenseTagStyle, |
| 83 | tag_location: Option<std::ops::Range<usize>>, |
| 84 | } |
| 85 | |
| 86 | impl<'a> SourceFileWithTags<'a> { |
| 87 | fn new(source: &'a str, style: &'a LicenseTagStyle) -> Self { |
| 88 | // This assumes that all LicenseTagStyle end with a SPDX license identifier line! |
| 89 | let location = match source.find(style.tag_start) { |
| 90 | Some(start) => { |
| 91 | let end_tag = source[start..] |
| 92 | .find(style.tag_end) |
| 93 | .map(|idx| start + idx + style.tag_end.len()) |
| 94 | .unwrap_or_default(); |
| 95 | if end_tag > start { |
| 96 | if style.is_real_end { |
| 97 | if source.as_bytes()[end_tag] == b' \n' { |
| 98 | Some(std::ops::Range { start, end: end_tag + 1 }) |
| 99 | } else { |
| 100 | Some(std::ops::Range { start, end: end_tag }) |
| 101 | } |
| 102 | } else { |
| 103 | let end = source[end_tag..] |
| 104 | .find(' \n' ) |
| 105 | .map(|idx| end_tag + idx + 1) |
| 106 | .unwrap_or(end_tag); |
| 107 | Some(std::ops::Range { start, end }) |
| 108 | } |
| 109 | } else { |
| 110 | None |
| 111 | } |
| 112 | } |
| 113 | None => None, |
| 114 | }; |
| 115 | |
| 116 | // Find default gettext copyright statements |
| 117 | let location = location.or_else(|| { |
| 118 | let Some(start) = |
| 119 | source.find("# SOME DESCRIPTIVE TITLE" ).or_else(|| source.find("# Copyright (C) " )) |
| 120 | else { |
| 121 | return None; |
| 122 | }; |
| 123 | let end_line = "# This file is distributed under the same license as the " ; |
| 124 | let Some(end) = source[start..].find(end_line) else { |
| 125 | return None; |
| 126 | }; |
| 127 | let end = start + end + end_line.len(); |
| 128 | let Some(end_nl) = source[end..].find(' \n' ) else { |
| 129 | return None; |
| 130 | }; |
| 131 | Some(std::ops::Range { start, end: end + end_nl + 1 }) |
| 132 | }); |
| 133 | |
| 134 | Self { source, tag_style: style, tag_location: location } |
| 135 | } |
| 136 | |
| 137 | fn has_tag(&self) -> bool { |
| 138 | self.tag_location.is_some() |
| 139 | } |
| 140 | |
| 141 | fn found_tag(&self) -> &'a str { |
| 142 | let tag_loc = match &self.tag_location { |
| 143 | Some(loc) => loc, |
| 144 | None => return "" , |
| 145 | }; |
| 146 | |
| 147 | &self.source[tag_loc.start..tag_loc.end] |
| 148 | } |
| 149 | |
| 150 | fn has_license_header(&self, expected_tag: &LicenseHeader) -> bool { |
| 151 | let tag_loc = match &self.tag_location { |
| 152 | Some(loc) => loc, |
| 153 | None => return false, |
| 154 | }; |
| 155 | |
| 156 | let found_tag = &self.source[tag_loc.start..tag_loc.end] |
| 157 | .trim_start_matches(self.tag_style.overall_start) |
| 158 | .trim_end_matches(self.tag_style.overall_end); |
| 159 | let mut tag_entries = found_tag.split(self.tag_style.line_break); |
| 160 | let Some(_copyright_entry) = tag_entries.next() else { return false }; |
| 161 | // Require _some_ license ... |
| 162 | let Some(_) = tag_entries.next() else { return false }; |
| 163 | // ... as well as the SPDX license line at the start |
| 164 | expected_tag.0 == SPDX_LICENSE_LINE |
| 165 | } |
| 166 | |
| 167 | fn replace_tag(&self, replacement: &LicenseHeader, license: &str) -> String { |
| 168 | let loc = &self.tag_location; |
| 169 | let next_char = if let Some(range) = loc { |
| 170 | self.source.as_bytes().get(range.end) |
| 171 | } else { |
| 172 | self.source.as_bytes().first() |
| 173 | }; |
| 174 | |
| 175 | let existing_copyright = loc.as_ref().and_then(|tag_loc| { |
| 176 | self.source[tag_loc.start..tag_loc.end] |
| 177 | .trim_start_matches(self.tag_style.overall_start) |
| 178 | .trim_end_matches(self.tag_style.overall_end) |
| 179 | .split(self.tag_style.line_break) |
| 180 | .next() |
| 181 | }); |
| 182 | |
| 183 | let new_header = [ |
| 184 | self.tag_style.overall_start, |
| 185 | &existing_copyright.map_or_else( |
| 186 | || { |
| 187 | [ |
| 188 | self.tag_style.line_prefix, |
| 189 | self.tag_style.line_indentation, |
| 190 | "Copyright © SixtyFPS GmbH <info@slint.dev>" , |
| 191 | ] |
| 192 | .concat() |
| 193 | }, |
| 194 | ToString::to_string, |
| 195 | ), |
| 196 | self.tag_style.line_break, |
| 197 | &replacement.to_string(self.tag_style, license), |
| 198 | self.tag_style.overall_end, |
| 199 | ] |
| 200 | .concat(); |
| 201 | let new_header = if next_char == Some(&b' \n' ) || next_char.is_none() { |
| 202 | new_header |
| 203 | } else { |
| 204 | format!(" {new_header}\n" ) |
| 205 | }; |
| 206 | |
| 207 | match loc { |
| 208 | Some(loc) => { |
| 209 | self.source[0..loc.start].to_string() + &new_header + &self.source[loc.end..] |
| 210 | } |
| 211 | None => new_header + self.source, |
| 212 | } |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | #[test ] |
| 217 | fn test_license_tag_c_style() { |
| 218 | let style = LicenseTagStyle::c_style_comment_style(); |
| 219 | { |
| 220 | let source = format!( |
| 221 | r#"// Copyright © something <bar@something.com> |
| 222 | foobar |
| 223 | // SP{}-License-Identifier: {} |
| 224 | blah"# , |
| 225 | "DX" , EXPECTED_SPDX_EXPRESSION |
| 226 | ); |
| 227 | let test_source = SourceFileWithTags::new(&source, &style); |
| 228 | assert_eq!( |
| 229 | test_source.replace_tag(&LicenseHeader("TEST_LICENSE" ), "foo" ), |
| 230 | r#"// Copyright © something <bar@something.com> |
| 231 | // TEST_LICENSE |
| 232 | |
| 233 | blah"# |
| 234 | .to_string() |
| 235 | ); |
| 236 | } |
| 237 | { |
| 238 | let source = format!( |
| 239 | r#"// Copyright © something <bar@something.com> |
| 240 | foobar |
| 241 | // SP{}-License-Identifier: {} |
| 242 | |
| 243 | blah"# , |
| 244 | "DX" , EXPECTED_SPDX_EXPRESSION |
| 245 | ); |
| 246 | let test_source = SourceFileWithTags::new(&source, &style); |
| 247 | assert_eq!( |
| 248 | test_source.replace_tag(&LicenseHeader("TEST_LICENSE" ), "bar" ), |
| 249 | r#"// Copyright © something <bar@something.com> |
| 250 | // TEST_LICENSE |
| 251 | |
| 252 | blah"# |
| 253 | .to_string() |
| 254 | ); |
| 255 | } |
| 256 | { |
| 257 | let test_source = SourceFileWithTags::new("blah" , &style); |
| 258 | assert_eq!( |
| 259 | test_source.replace_tag(&LicenseHeader("TEST_LICENSE" ), "bar" ), |
| 260 | r#"// Copyright © SixtyFPS GmbH <info@slint.dev> |
| 261 | // TEST_LICENSE |
| 262 | |
| 263 | blah"# |
| 264 | .to_string() |
| 265 | ); |
| 266 | } |
| 267 | { |
| 268 | let test_source = SourceFileWithTags::new(" \nblah" , &style); |
| 269 | assert_eq!( |
| 270 | test_source.replace_tag(&LicenseHeader(SPDX_LICENSE_LINE), "bar" ), |
| 271 | String::from("// Copyright © SixtyFPS GmbH <info@slint.dev> \n// " ) |
| 272 | + SPDX_LICENSE_LINE |
| 273 | + r#"bar |
| 274 | |
| 275 | blah"# |
| 276 | ); |
| 277 | } |
| 278 | { |
| 279 | let test_source = SourceFileWithTags::new("" , &style); |
| 280 | assert_eq!( |
| 281 | test_source.replace_tag(&LicenseHeader("TEST_LICENSE" ), "bar" ), |
| 282 | r#"// Copyright © SixtyFPS GmbH <info@slint.dev> |
| 283 | // TEST_LICENSE |
| 284 | "# |
| 285 | .to_string() |
| 286 | ); |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | #[test ] |
| 291 | fn test_license_tag_hash() { |
| 292 | let style = LicenseTagStyle::shell_comment_style(); |
| 293 | { |
| 294 | let source = format!( |
| 295 | r#"# Copyright © something <bar@something.com> |
| 296 | foobar |
| 297 | # SP{}-License-Identifier: {} |
| 298 | |
| 299 | blah"# , |
| 300 | "DX" , EXPECTED_SPDX_EXPRESSION |
| 301 | ); |
| 302 | let test_source = SourceFileWithTags::new(&source, &style); |
| 303 | assert_eq!( |
| 304 | test_source.replace_tag(&LicenseHeader("TEST_LICENSE" ), "bar" ), |
| 305 | r#"# Copyright © something <bar@something.com> |
| 306 | # TEST_LICENSE |
| 307 | |
| 308 | blah"# |
| 309 | .to_string() |
| 310 | ); |
| 311 | } |
| 312 | { |
| 313 | let test_source = SourceFileWithTags::new(r#"blah"# , &style); |
| 314 | assert_eq!( |
| 315 | test_source.replace_tag(&LicenseHeader("TEST_LICENSE" ), "bar" ), |
| 316 | r#"# Copyright © SixtyFPS GmbH <info@slint.dev> |
| 317 | # TEST_LICENSE |
| 318 | |
| 319 | blah"# |
| 320 | .to_string() |
| 321 | ); |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | #[test ] |
| 326 | fn test_license_tag_dotdot() { |
| 327 | let style = LicenseTagStyle::rst_comment_style(); |
| 328 | { |
| 329 | let source = format!( |
| 330 | r#".. Copyright © something <bar@something.com> |
| 331 | foobar |
| 332 | .. SP{}-License-Identifier: {} |
| 333 | |
| 334 | blah"# , |
| 335 | "DX" , EXPECTED_SPDX_EXPRESSION |
| 336 | ); |
| 337 | let test_source = SourceFileWithTags::new(&source, &style); |
| 338 | assert_eq!( |
| 339 | test_source.replace_tag(&LicenseHeader("TEST_LICENSE" ), "bar" ), |
| 340 | r#".. Copyright © something <bar@something.com> |
| 341 | .. TEST_LICENSE |
| 342 | |
| 343 | blah"# |
| 344 | .to_string() |
| 345 | ); |
| 346 | } |
| 347 | { |
| 348 | let test_source = SourceFileWithTags::new(r#"blah"# , &style); |
| 349 | assert_eq!( |
| 350 | test_source.replace_tag(&LicenseHeader("TEST_LICENSE" ), "bar" ), |
| 351 | r#".. Copyright © SixtyFPS GmbH <info@slint.dev> |
| 352 | .. TEST_LICENSE |
| 353 | |
| 354 | blah"# |
| 355 | .to_string() |
| 356 | ); |
| 357 | } |
| 358 | } |
| 359 | |
| 360 | #[test ] |
| 361 | fn test_license_tag_html_style() { |
| 362 | let style = LicenseTagStyle::html_comment_style(); |
| 363 | { |
| 364 | let source = format!( |
| 365 | r#"<!-- Copyright © something <bar@something.com> ; SP{}-License-Identifier: {} --> |
| 366 | blah"# , |
| 367 | "DX" , EXPECTED_SPDX_EXPRESSION |
| 368 | ); |
| 369 | let test_source = SourceFileWithTags::new(&source, &style); |
| 370 | assert_eq!( |
| 371 | test_source.replace_tag(&LicenseHeader("TEST_LICENSE" ), "foo" ), |
| 372 | r#"<!-- Copyright © something <bar@something.com> ; TEST_LICENSE --> |
| 373 | |
| 374 | blah"# |
| 375 | .to_string() |
| 376 | ); |
| 377 | } |
| 378 | { |
| 379 | let source = format!( |
| 380 | r#"<!-- Copyright © something <bar@something.com> ; SP{}-License-Identifier: {} --> |
| 381 | |
| 382 | blah"# , |
| 383 | "DX" , EXPECTED_SPDX_EXPRESSION |
| 384 | ); |
| 385 | let test_source = SourceFileWithTags::new(&source, &style); |
| 386 | assert_eq!( |
| 387 | test_source.replace_tag(&LicenseHeader("TEST_LICENSE" ), "bar" ), |
| 388 | r#"<!-- Copyright © something <bar@something.com> ; TEST_LICENSE --> |
| 389 | |
| 390 | blah"# |
| 391 | .to_string() |
| 392 | ); |
| 393 | } |
| 394 | { |
| 395 | let test_source = SourceFileWithTags::new("blah" , &style); |
| 396 | assert_eq!( |
| 397 | test_source.replace_tag(&LicenseHeader("TEST_LICENSE" ), "bar" ), |
| 398 | r#"<!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; TEST_LICENSE --> |
| 399 | |
| 400 | blah"# |
| 401 | .to_string() |
| 402 | ); |
| 403 | } |
| 404 | { |
| 405 | let test_source = SourceFileWithTags::new(" \nblah" , &style); |
| 406 | assert_eq!( |
| 407 | test_source.replace_tag(&LicenseHeader(SPDX_LICENSE_LINE), "bar" ), |
| 408 | String::from("<!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; " ) |
| 409 | + SPDX_LICENSE_LINE |
| 410 | + r#"bar --> |
| 411 | |
| 412 | blah"# |
| 413 | ); |
| 414 | } |
| 415 | { |
| 416 | let test_source = SourceFileWithTags::new("" , &style); |
| 417 | assert_eq!( |
| 418 | test_source.replace_tag(&LicenseHeader("TEST_LICENSE" ), "bar" ), |
| 419 | r#"<!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; TEST_LICENSE --> |
| 420 | "# |
| 421 | .to_string() |
| 422 | ); |
| 423 | } |
| 424 | } |
| 425 | |
| 426 | #[derive (Copy, Clone, Debug)] |
| 427 | enum LicenseLocation { |
| 428 | Tag(LicenseTagStyle), |
| 429 | Crate, |
| 430 | NoLicense, |
| 431 | } |
| 432 | |
| 433 | lazy_static! { |
| 434 | // cspell:disable |
| 435 | static ref LICENSE_LOCATION_FOR_FILE: Vec<(regex::Regex, LicenseLocation)> = [ |
| 436 | // full matches |
| 437 | ("^ \\.cargo/config$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 438 | ("^ \\.clang-format$" , LicenseLocation::NoLicense), |
| 439 | ("^ \\.github/.* \\.md$" , LicenseLocation::NoLicense), |
| 440 | ("^ \\.mailmap$" , LicenseLocation::NoLicense), |
| 441 | ("^ \\.mise/tasks/" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 442 | ("^api/cpp/docs/conf \\.py$" , LicenseLocation::NoLicense), |
| 443 | ("^docs/reference/Pipfile$" , LicenseLocation::NoLicense), |
| 444 | ("^docs/reference/conf \\.py$" , LicenseLocation::NoLicense), |
| 445 | ("^editors/vscode/src/snippets \\.ts$" , LicenseLocation::NoLicense), // liberal license |
| 446 | ("^editors/vscode/tests/grammar/.* \\.slint$" , LicenseLocation::NoLicense), // License header breaks these tests |
| 447 | ("^editors/tree-sitter-slint/binding \\.gyp$" , LicenseLocation::NoLicense), // liberal license |
| 448 | ("^editors/tree-sitter-slint/test-to-corpus \\.py$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 449 | ("^Cargo \\.lock$" , LicenseLocation::NoLicense), |
| 450 | ("^demos/printerdemo/zephyr/VERSION$" , LicenseLocation::NoLicense), |
| 451 | ("^examples/mcu-board-support/pico2_st7789/rp_pico2.rs$" , LicenseLocation::NoLicense), // third-party file |
| 452 | |
| 453 | // filename based matches: |
| 454 | ("(^|/)CMakeLists \\.txt$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 455 | ("(^|/)Cargo \\.toml$" , LicenseLocation::Crate), |
| 456 | ("(^|/)Dockerfile" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 457 | ("(^|/)LICENSE$" , LicenseLocation::NoLicense), |
| 458 | ("(^|/)LICENSE \\.QT$" , LicenseLocation::NoLicense), |
| 459 | ("(^|/)README$" , LicenseLocation::NoLicense), |
| 460 | ("(^|/) \\.eslintrc \\.yml$" , LicenseLocation::NoLicense), |
| 461 | ("(^|/)memory \\.x$" , LicenseLocation::NoLicense), // third-party file |
| 462 | ("(^|/)webpack \\..+ \\.js$" , LicenseLocation::NoLicense), |
| 463 | ("(^|/)partitions \\.csv$" , LicenseLocation::NoLicense), |
| 464 | ("(^|/)sdkconfig" , LicenseLocation::NoLicense), // auto-generated |
| 465 | ("(^|/)Pipfile$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 466 | ("(^|/) \\.npmrc$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 467 | ("(^|/)pnpm-lock \\.yaml$" , LicenseLocation::NoLicense), |
| 468 | ("(^|/)biome \\.json$" , LicenseLocation::NoLicense), |
| 469 | ("(^|/)package-lock \\.json$" , LicenseLocation::NoLicense), |
| 470 | ("(^|/)py.typed$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 471 | |
| 472 | // Path prefix matches: |
| 473 | ("^editors/tree-sitter-slint/corpus/" , LicenseLocation::NoLicense), // liberal license |
| 474 | ("^api/cpp/docs/_static/" , LicenseLocation::NoLicense), |
| 475 | ("^api/cpp/docs/_templates/" , LicenseLocation::NoLicense), |
| 476 | ("^docs/quickstart/theme/" , LicenseLocation::NoLicense), |
| 477 | ("^editors/tree-sitter-slint/queries/" , LicenseLocation::NoLicense), // liberal license |
| 478 | |
| 479 | // directory based matches |
| 480 | ("(^|/)LICENSES/" , LicenseLocation::NoLicense), |
| 481 | |
| 482 | // Extension matches: |
| 483 | (" \\.60$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 484 | (" \\.60 \\.disabled$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 485 | (" \\.astro$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 486 | (" \\.cmake$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 487 | (" \\.cmake.in$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 488 | (" \\.conf$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 489 | (" \\.cpp$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 490 | (" \\.css$" , LicenseLocation::NoLicense), |
| 491 | (" \\.gitattributes$" , LicenseLocation::NoLicense), |
| 492 | (" \\.gitignore$" , LicenseLocation::NoLicense), |
| 493 | (" \\.vscodeignore$" , LicenseLocation::NoLicense), |
| 494 | (" \\.dockerignore$" , LicenseLocation::NoLicense), |
| 495 | (" \\.dockerignore$" , LicenseLocation::NoLicense), |
| 496 | (" \\.prettierignore$" , LicenseLocation::NoLicense), |
| 497 | (" \\.bazelignore$" , LicenseLocation::NoLicense), |
| 498 | (" \\.npmignore$" , LicenseLocation::NoLicense), |
| 499 | (" \\.h$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 500 | (" \\.html$" , LicenseLocation::NoLicense), |
| 501 | (" \\.java$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 502 | (" \\.jpg$" , LicenseLocation::NoLicense), |
| 503 | (" \\.js$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 504 | (" \\.json$" , LicenseLocation::NoLicense), |
| 505 | (" \\.jsonc$" , LicenseLocation::NoLicense), |
| 506 | (" \\.license$" , LicenseLocation::NoLicense), |
| 507 | (" \\.md$" , LicenseLocation::Tag(LicenseTagStyle::html_comment_style())), |
| 508 | (" \\.mdx$" , LicenseLocation::Tag(LicenseTagStyle::html_comment_style())), |
| 509 | (" \\.mjs$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 510 | (" \\.mts$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 511 | (" \\.hbs$" , LicenseLocation::Tag(LicenseTagStyle::html_comment_style())), |
| 512 | (" \\.overlay$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 513 | (" \\.pdf$" , LicenseLocation::NoLicense), |
| 514 | (" \\.png$" , LicenseLocation::NoLicense), |
| 515 | (" \\.mo$" , LicenseLocation::NoLicense), |
| 516 | (" \\.po$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 517 | (" \\.pot$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 518 | (" \\.rs$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 519 | (" \\.rst$" , LicenseLocation::Tag(LicenseTagStyle::rst_comment_style())), |
| 520 | (" \\.sh$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 521 | (" \\.slint$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 522 | (" \\.slint \\.disabled$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 523 | (" \\.sublime-commands$" , LicenseLocation::NoLicense), |
| 524 | (" \\.sublime-settings$" , LicenseLocation::NoLicense), |
| 525 | (" \\.sublime-syntax$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 526 | (" \\.svg$" , LicenseLocation::NoLicense), |
| 527 | (" \\.tmPreferences$" , LicenseLocation::NoLicense), |
| 528 | (" \\.toml$" , LicenseLocation::NoLicense), |
| 529 | (" \\.ts$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 530 | (" \\.tsx$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 531 | (" \\.ttf$" , LicenseLocation::NoLicense), |
| 532 | (" \\.txt$" , LicenseLocation::NoLicense), |
| 533 | (" \\.ui$" , LicenseLocation::NoLicense), |
| 534 | (" \\.webp$" , LicenseLocation::NoLicense), |
| 535 | (" \\.xml$" , LicenseLocation::NoLicense), |
| 536 | (" \\.yaml$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 537 | (" \\.yml$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 538 | (" \\.py$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 539 | (" \\.pyi$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 540 | (" \\.proto$" , LicenseLocation::Tag(LicenseTagStyle::c_style_comment_style())), |
| 541 | (" \\.bazelrc$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 542 | ("MODULE.bazel$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 543 | ("BUILD.bazel$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), |
| 544 | ("MODULE.bazel.lock$" , LicenseLocation::NoLicense), |
| 545 | (" \\.patch$" , LicenseLocation::Tag(LicenseTagStyle::shell_comment_style())), // Doesn't really need a # prefix, but better than nothing |
| 546 | (" \\.bazelversion$" , LicenseLocation::NoLicense), |
| 547 | ] |
| 548 | .iter() |
| 549 | .map(|(re, ty)| (regex::Regex::new(re).unwrap(), *ty)) |
| 550 | .collect(); |
| 551 | } |
| 552 | |
| 553 | lazy_static! { |
| 554 | static ref LICENSE_FOR_FILE: Vec<(regex::Regex, &'static str)> = [ |
| 555 | ("^editors/tree-sitter-slint/grammar.js$" , MIT_LICENSE), |
| 556 | ("^helper_crates/const-field-offset/" , MIT_OR_APACHE2_LICENSE), |
| 557 | ("^helper_crates/vtable/" , MIT_OR_APACHE2_LICENSE), |
| 558 | ("^api/cpp/esp-idf/LICENSE$" , TRIPLE_LICENSE), |
| 559 | ("^examples/" , MIT_LICENSE), |
| 560 | ("^demos/" , MIT_LICENSE), |
| 561 | ("^docs/" , MIT_LICENSE), |
| 562 | ("^api/cpp/docs/" , MIT_LICENSE), |
| 563 | ("(^|/)(README|CONTRIBUTING|CHANGELOG|LICENSE) \\.md" , TRIPLE_LICENSE), |
| 564 | (".* \\.md$" , MIT_LICENSE), |
| 565 | (".*" , TRIPLE_LICENSE), |
| 566 | ] |
| 567 | .iter() |
| 568 | .map(|(re, ty)| (regex::Regex::new(re).unwrap(), *ty)) |
| 569 | .collect(); |
| 570 | // cspell:enable |
| 571 | } |
| 572 | |
| 573 | const TRIPLE_LICENSE: &str = |
| 574 | "GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0" ; |
| 575 | const MIT_LICENSE: &str = "MIT" ; |
| 576 | const MIT_OR_APACHE2_LICENSE: &str = "MIT OR Apache-2.0" ; |
| 577 | |
| 578 | // This is really just the SPDX expression after the copyright line. The existence of the |
| 579 | // Copyright prefix is enforced by the tag scanning (tag_start). |
| 580 | pub struct LicenseHeader<'a>(&'a str); |
| 581 | |
| 582 | impl LicenseHeader<'_> { |
| 583 | fn to_string(&self, style: &LicenseTagStyle, license: &str) -> String { |
| 584 | let mut result: String = [style.line_prefix, style.line_indentation, self.0].concat(); |
| 585 | |
| 586 | if self.0 == SPDX_LICENSE_LINE { |
| 587 | result.push_str(string:license); |
| 588 | } |
| 589 | |
| 590 | result |
| 591 | } |
| 592 | } |
| 593 | |
| 594 | #[cfg (test)] |
| 595 | const EXPECTED_SPDX_EXPRESSION: &str = |
| 596 | "GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0" ; |
| 597 | |
| 598 | const SPDX_LICENSE_ID: &str = const_format::concatcp!("SP" , "DX-License-Identifier:" ); // Do not confuse the reuse tool |
| 599 | const SPDX_LICENSE_LINE: &str = const_format::concatcp!(SPDX_LICENSE_ID, " " ); // Do not confuse the reuse tool |
| 600 | |
| 601 | const EXPECTED_HEADER: LicenseHeader<'static> = LicenseHeader(SPDX_LICENSE_LINE); |
| 602 | |
| 603 | const EXPECTED_HOMEPAGE: &str = "https://slint.dev" ; |
| 604 | const ALLOWED_HOMEPAGE: &str = "https://slint.rs" ; |
| 605 | const EXPECTED_REPOSITORY: &str = "https://github.com/slint-ui/slint" ; |
| 606 | |
| 607 | fn collect_files() -> Result<Vec<PathBuf>> { |
| 608 | let root = super::root_dir(); |
| 609 | |
| 610 | let mut files = Vec::new(); |
| 611 | let (ls_files_output, split_char) = if root.join(".jj" ).exists() { |
| 612 | ( |
| 613 | super::run_command( |
| 614 | "jj" , |
| 615 | &["file" , "list" ], |
| 616 | std::iter::empty::<(std::ffi::OsString, std::ffi::OsString)>(), |
| 617 | )? |
| 618 | .stdout, |
| 619 | b' \n' , |
| 620 | ) |
| 621 | } else { |
| 622 | ( |
| 623 | super::run_command( |
| 624 | "git" , |
| 625 | &["ls-files" , "-z" ], |
| 626 | std::iter::empty::<(std::ffi::OsString, std::ffi::OsString)>(), |
| 627 | )? |
| 628 | .stdout, |
| 629 | b' \0' , |
| 630 | ) |
| 631 | }; |
| 632 | |
| 633 | for path in ls_files_output.split(|ch| *ch == split_char) { |
| 634 | if path.is_empty() { |
| 635 | continue; |
| 636 | } |
| 637 | let path = PathBuf::from_str( |
| 638 | std::str::from_utf8(path) |
| 639 | .context("Error decoding file list command output from VCS as utf-8" )?, |
| 640 | ) |
| 641 | .context("Failed to decide path output in VCS file list" )?; |
| 642 | |
| 643 | if !path.is_dir() { |
| 644 | files.push(root.join(path)); |
| 645 | } |
| 646 | } |
| 647 | |
| 648 | Ok(files) |
| 649 | } |
| 650 | |
| 651 | #[derive (Debug)] |
| 652 | enum CargoDependency { |
| 653 | Workspace, |
| 654 | Full { path: String, version: String }, |
| 655 | } |
| 656 | |
| 657 | impl CargoDependency { |
| 658 | fn new(encoded_value: &toml_edit::Value) -> Option<Self> { |
| 659 | match encoded_value { |
| 660 | toml_edit::Value::String(s) => { |
| 661 | Some(Self::Full { version: s.value().clone(), path: String::new() }) |
| 662 | } |
| 663 | toml_edit::Value::Float(_) => None, |
| 664 | toml_edit::Value::Datetime(_) => None, |
| 665 | toml_edit::Value::Boolean(_) => None, |
| 666 | toml_edit::Value::Array(_) => None, |
| 667 | toml_edit::Value::Integer(_) => None, |
| 668 | toml_edit::Value::InlineTable(table) => { |
| 669 | if table.get("workspace" ).is_some() { |
| 670 | Some(Self::Workspace) |
| 671 | } else { |
| 672 | Some(Self::Full { |
| 673 | path: table.get("path" ).and_then(|x| x.as_str()).unwrap_or("" ).to_owned(), |
| 674 | version: table |
| 675 | .get("version" ) |
| 676 | .and_then(|x| x.as_str()) |
| 677 | .unwrap_or("" ) |
| 678 | .to_owned(), |
| 679 | }) |
| 680 | } |
| 681 | } |
| 682 | } |
| 683 | } |
| 684 | } |
| 685 | |
| 686 | struct CargoToml { |
| 687 | path: std::path::PathBuf, |
| 688 | doc: toml_edit::DocumentMut, |
| 689 | edited: bool, |
| 690 | } |
| 691 | |
| 692 | impl CargoToml { |
| 693 | fn new(path: &Path) -> Result<Self> { |
| 694 | let source = &std::fs::read_to_string(path).context("Error reading file" )?; |
| 695 | Ok(Self { doc: source.parse()?, edited: false, path: path.to_path_buf() }) |
| 696 | } |
| 697 | |
| 698 | fn is_workspace(&self) -> bool { |
| 699 | self.doc.as_table().get("workspace" ).is_some() |
| 700 | } |
| 701 | |
| 702 | fn workspace_version(&self) -> Result<&str> { |
| 703 | if !self.is_workspace() { |
| 704 | Err(anyhow!("Not a workspace, can not extract version info." )) |
| 705 | } else { |
| 706 | Ok(self |
| 707 | .doc |
| 708 | .as_table() |
| 709 | .get("workspace" ) |
| 710 | .unwrap() |
| 711 | .get("package" ) |
| 712 | .unwrap() |
| 713 | .get("version" ) |
| 714 | .unwrap() |
| 715 | .as_str() |
| 716 | .unwrap()) |
| 717 | } |
| 718 | } |
| 719 | |
| 720 | fn package(&self) -> Result<&toml_edit::Table> { |
| 721 | if self.is_workspace() { |
| 722 | self.doc |
| 723 | .as_table() |
| 724 | .get("workspace" ) |
| 725 | .and_then(|w| w.as_table()) |
| 726 | .and_then(|w| w.get("package" )) |
| 727 | .and_then(|p| p.as_table()) |
| 728 | .ok_or_else(|| anyhow!("Invalid Cargo.toml -- cannot find workspace package section in workspace file" )) |
| 729 | } else { |
| 730 | self.doc |
| 731 | .as_table() |
| 732 | .get("package" ) |
| 733 | .and_then(|p| p.as_table()) |
| 734 | .ok_or_else(|| anyhow!("Invalid Cargo.toml -- cannot find package section" )) |
| 735 | } |
| 736 | } |
| 737 | |
| 738 | fn dependencies(&self, dep_type: &str) -> Vec<(String, CargoDependency)> { |
| 739 | let table = if self.is_workspace() { |
| 740 | self.doc |
| 741 | .as_table() |
| 742 | .get("workspace" ) |
| 743 | .and_then(|w| w.as_table()) |
| 744 | .and_then(|w| w.get(dep_type)) |
| 745 | .and_then(|d| d.as_table()) |
| 746 | } else { |
| 747 | self.doc.as_table().get(dep_type).and_then(|d| d.as_table()) |
| 748 | }; |
| 749 | match table { |
| 750 | Some(dep_table) => dep_table |
| 751 | .iter() |
| 752 | .filter_map(|(name, entry)| { |
| 753 | entry.as_value().and_then(|entry| { |
| 754 | CargoDependency::new(entry).map(|entry| (name.to_owned(), entry)) |
| 755 | }) |
| 756 | }) |
| 757 | .collect(), |
| 758 | None => Vec::new(), |
| 759 | } |
| 760 | } |
| 761 | |
| 762 | fn check_and_fix_package_string_field<'a>( |
| 763 | &mut self, |
| 764 | fix_it: bool, |
| 765 | field: &'a str, |
| 766 | expected_str: &'a str, |
| 767 | ) -> Result<()> { |
| 768 | let is_workspace = self.is_workspace(); |
| 769 | match self.package()?.get(field) { |
| 770 | Some(field_value) => { |
| 771 | match field_value.get("workspace" ).and_then(|v| v.as_bool()) { |
| 772 | Some(true) if is_workspace => { |
| 773 | return Err(anyhow!( |
| 774 | "Using workspace {}.workspace = true in workspace" , |
| 775 | field |
| 776 | )) |
| 777 | } |
| 778 | Some(true) => { /* nothing to do */ } |
| 779 | Some(false) => { |
| 780 | if fix_it { |
| 781 | eprintln!( |
| 782 | "Fixing up {:?} as instructed. It has {field}.workspace = false" , |
| 783 | self.path |
| 784 | ); |
| 785 | self.doc["package" ][field]["workspace" ] = toml_edit::value(true); |
| 786 | self.edited = true; |
| 787 | } else { |
| 788 | return Err(anyhow!( |
| 789 | "Incorrect {}.workspace found: expected true, found false" , |
| 790 | field, |
| 791 | )); |
| 792 | } |
| 793 | } |
| 794 | None => match field_value.as_str() { |
| 795 | Some(text) => { |
| 796 | if text != expected_str { |
| 797 | if fix_it { |
| 798 | eprintln!("Fixing up {:?} as instructed. It has unexpected data in {field}." , self.path); |
| 799 | self.doc["package" ][field] = toml_edit::value(expected_str); |
| 800 | self.edited = true; |
| 801 | } else { |
| 802 | return Err(anyhow!( |
| 803 | "Incorrect {}. Found {} expected {}" , |
| 804 | field, |
| 805 | text, |
| 806 | expected_str |
| 807 | )); |
| 808 | } |
| 809 | } |
| 810 | } |
| 811 | None => return Err(anyhow!(" {} field is not a string" , field)), |
| 812 | }, |
| 813 | }; |
| 814 | } |
| 815 | None => { |
| 816 | if fix_it { |
| 817 | eprintln!("Fixing up {:?} as instructed. It has no {field}." , self.path); |
| 818 | self.doc["package" ][field] = toml_edit::value(expected_str); |
| 819 | self.edited = true; |
| 820 | } else { |
| 821 | return Err(anyhow!("Missing {} field" , field)); |
| 822 | } |
| 823 | } |
| 824 | }; |
| 825 | Ok(()) |
| 826 | } |
| 827 | |
| 828 | fn published(&self) -> Result<bool> { |
| 829 | Ok(self.package()?.get("publish" ).map(|v| v.as_bool().unwrap()).unwrap_or(true)) |
| 830 | } |
| 831 | |
| 832 | fn save_if_changed(&self) -> Result<()> { |
| 833 | if self.edited { |
| 834 | std::fs::write(&self.path, self.doc.to_string()).context("Error writing new Cargo.toml" ) |
| 835 | } else { |
| 836 | Ok(()) |
| 837 | } |
| 838 | } |
| 839 | } |
| 840 | |
| 841 | #[derive (Debug, clap::Parser)] |
| 842 | pub struct LicenseHeaderCheck { |
| 843 | #[arg(long, action)] |
| 844 | fix_it: bool, |
| 845 | |
| 846 | #[arg(long, action)] |
| 847 | show_all: bool, |
| 848 | |
| 849 | #[arg(long, action)] |
| 850 | verbose: bool, |
| 851 | |
| 852 | #[arg(skip)] |
| 853 | workspace_version: RefCell<String>, |
| 854 | } |
| 855 | |
| 856 | impl LicenseHeaderCheck { |
| 857 | pub fn check_license_headers(&self) -> Result<(), Box<dyn std::error::Error>> { |
| 858 | let mut seen_errors = false; |
| 859 | for path in &collect_files()? { |
| 860 | let result = self |
| 861 | .check_file(path.as_path()) |
| 862 | .with_context(|| format!("checking {}" , &path.to_string_lossy())); |
| 863 | |
| 864 | if result.is_err() { |
| 865 | seen_errors = true; |
| 866 | if self.show_all { |
| 867 | eprintln!("Error: {result:?}" ); |
| 868 | } else { |
| 869 | return result.map_err(|e| e.into()); |
| 870 | } |
| 871 | } |
| 872 | } |
| 873 | if seen_errors { |
| 874 | Err(anyhow!("Encountered one or multiple errors. See above for details." ).into()) |
| 875 | } else { |
| 876 | println!("All files are ok." ); |
| 877 | Ok(()) |
| 878 | } |
| 879 | } |
| 880 | |
| 881 | fn check_file_tags(&self, path: &Path, style: &LicenseTagStyle, license: &str) -> Result<()> { |
| 882 | let source = &std::fs::read_to_string(path).context("Error reading file" )?; |
| 883 | |
| 884 | let source = SourceFileWithTags::new(source, style); |
| 885 | |
| 886 | if !source.has_tag() { |
| 887 | if self.fix_it { |
| 888 | eprintln!("Fixing up {path:?} as instructed. It's missing a license header." ); |
| 889 | let source = source.replace_tag(&EXPECTED_HEADER, license); |
| 890 | std::fs::write(path, source).context("Error writing source" ) |
| 891 | } else { |
| 892 | Err(anyhow!("Missing tag" )) |
| 893 | } |
| 894 | } else if source.has_license_header(&EXPECTED_HEADER) { |
| 895 | Ok(()) |
| 896 | } else if self.fix_it { |
| 897 | eprintln!("Fixing up {path:?} as instructed. It has a wrong license header." ); |
| 898 | let source = source.replace_tag(&EXPECTED_HEADER, license); |
| 899 | std::fs::write(path, source).context("Error writing new source" ) |
| 900 | } else { |
| 901 | Err(anyhow!( |
| 902 | "unexpected header. \nexpected: {}\nfound: {}" , |
| 903 | EXPECTED_HEADER.to_string(style, license), |
| 904 | source.found_tag() |
| 905 | )) |
| 906 | } |
| 907 | } |
| 908 | |
| 909 | fn check_cargo_toml(&self, path: &Path, license: &str) -> Result<()> { |
| 910 | let mut doc = CargoToml::new(path)?; |
| 911 | |
| 912 | if doc.is_workspace() { |
| 913 | let mut wv = self.workspace_version.borrow_mut(); |
| 914 | if &*wv == "" { |
| 915 | *wv = doc.workspace_version()?.to_string(); |
| 916 | } |
| 917 | let expected_version = wv.clone(); |
| 918 | |
| 919 | // Check the workspace.dependencies: |
| 920 | self.check_dependencies(&doc, &expected_version)?; |
| 921 | |
| 922 | return Ok(()); |
| 923 | } |
| 924 | |
| 925 | doc.check_and_fix_package_string_field(self.fix_it, "license" , license)?; |
| 926 | |
| 927 | if !doc.published()? { |
| 928 | // Skip further tests for package that are not published |
| 929 | return doc.save_if_changed(); |
| 930 | } |
| 931 | |
| 932 | if doc.check_and_fix_package_string_field(false, "homepage" , ALLOWED_HOMEPAGE).is_err() { |
| 933 | doc.check_and_fix_package_string_field(self.fix_it, "homepage" , EXPECTED_HOMEPAGE)?; |
| 934 | } |
| 935 | doc.check_and_fix_package_string_field(self.fix_it, "repository" , EXPECTED_REPOSITORY)?; |
| 936 | |
| 937 | if doc.package()?["description" ].is_none() { |
| 938 | return Err(anyhow!("Missing description field" )); |
| 939 | } |
| 940 | |
| 941 | // Check that version of slint- dependencies are matching this version |
| 942 | let expected_version = { |
| 943 | let wv = self.workspace_version.borrow().clone(); |
| 944 | doc.package()? |
| 945 | .get("version" ) |
| 946 | .and_then(|v| v.as_value()) |
| 947 | .and_then(|v| v.as_str()) |
| 948 | .unwrap_or(&wv) |
| 949 | .to_string() |
| 950 | }; |
| 951 | |
| 952 | self.check_dependencies(&doc, &expected_version)?; |
| 953 | |
| 954 | doc.save_if_changed() |
| 955 | } |
| 956 | |
| 957 | fn check_dependencies(&self, doc: &CargoToml, expected_version: &str) -> Result<()> { |
| 958 | let expected_version = format!("= {expected_version}" ); |
| 959 | |
| 960 | for (dep_name, dep) in doc |
| 961 | .dependencies("dependencies" ) |
| 962 | .iter() |
| 963 | .chain(doc.dependencies("build-dependencies" ).iter()) |
| 964 | { |
| 965 | if dep_name.starts_with("slint" ) || dep_name.starts_with("i-slint" ) { |
| 966 | match dep { |
| 967 | CargoDependency::Workspace => (), |
| 968 | CargoDependency::Full { path, version } => { |
| 969 | if path.is_empty() { |
| 970 | return Err(anyhow!( |
| 971 | "slint package ' {}' outside of the repository?" , |
| 972 | dep_name |
| 973 | )); |
| 974 | } |
| 975 | if version != &expected_version { |
| 976 | return Err(anyhow!( |
| 977 | "Version \"{}\" must be specified for dependency {}" , |
| 978 | expected_version, |
| 979 | dep_name |
| 980 | )); |
| 981 | } |
| 982 | } |
| 983 | } |
| 984 | } |
| 985 | } |
| 986 | |
| 987 | for (dep_name, dep) in doc.dependencies("dev-dependencies" ).iter() { |
| 988 | if dep_name.starts_with("slint" ) || dep_name.starts_with("i-slint" ) { |
| 989 | match dep { |
| 990 | CargoDependency::Workspace => { |
| 991 | return Err(anyhow!( |
| 992 | "dev-dependencies cannot be from the workspace because version must be empty {dep_name}" |
| 993 | )); |
| 994 | } |
| 995 | CargoDependency::Full { path, version } => { |
| 996 | if path.is_empty() { |
| 997 | return Err(anyhow!( |
| 998 | "slint package ' {}' outside of the repository?" , |
| 999 | dep_name |
| 1000 | )); |
| 1001 | } |
| 1002 | if !version.is_empty() { |
| 1003 | return Err(anyhow!( |
| 1004 | "dev-dependencies version must be empty for dependency {dep_name}" |
| 1005 | )); |
| 1006 | } |
| 1007 | } |
| 1008 | } |
| 1009 | } |
| 1010 | } |
| 1011 | Ok(()) |
| 1012 | } |
| 1013 | |
| 1014 | fn check_file(&self, path: &Path) -> Result<()> { |
| 1015 | let repo_relative_path = path.strip_prefix(super::root_dir())?; |
| 1016 | let path_str = repo_relative_path.to_str().unwrap(); |
| 1017 | let location = LICENSE_LOCATION_FOR_FILE |
| 1018 | .iter() |
| 1019 | .find_map(|(regex, style)| if regex.is_match(path_str) { Some(style) } else { None }) |
| 1020 | .with_context(|| "Cannot determine the expected license header style. Please fix the license checking xtask." )?; |
| 1021 | |
| 1022 | let license = LICENSE_FOR_FILE |
| 1023 | .iter() |
| 1024 | .find_map( |
| 1025 | |(regex, license)| if regex.is_match(path_str) { Some(license) } else { None }, |
| 1026 | ) |
| 1027 | .with_context(|| { |
| 1028 | "Cannot determine the expected license. Please fix the license checking xtask." |
| 1029 | })?; |
| 1030 | |
| 1031 | match location { |
| 1032 | LicenseLocation::Tag(tag_style) => self.check_file_tags(path, tag_style, license), |
| 1033 | LicenseLocation::Crate => { |
| 1034 | self.check_file_tags(path, &LicenseTagStyle::shell_comment_style(), license)?; |
| 1035 | self.check_cargo_toml(path, license) |
| 1036 | } |
| 1037 | LicenseLocation::NoLicense => { |
| 1038 | if self.verbose { |
| 1039 | println!("Skipping {path_str} as configured" ); |
| 1040 | } |
| 1041 | Ok(()) |
| 1042 | } |
| 1043 | } |
| 1044 | } |
| 1045 | } |
| 1046 | |