mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00

* Skip linting target folder Contains build files not passing the copyright header check. * Implicitly clear duplicate keys when serializing Fixes `originalStockKind` not being cleared from `other`, as it had mistakenly been added to the field list for `NoteFieldSchema11`.
225 lines
6.8 KiB
Rust
225 lines
6.8 KiB
Rust
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
use std::collections::HashSet;
|
|
use std::env;
|
|
use std::fs;
|
|
use std::fs::File;
|
|
use std::io::Read;
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use camino::Utf8Path;
|
|
use once_cell::unsync::Lazy;
|
|
use walkdir::WalkDir;
|
|
|
|
const NONSTANDARD_HEADER: &[&str] = &[
|
|
"./pylib/anki/_vendor/stringcase.py",
|
|
"./pylib/anki/importing/pauker.py",
|
|
"./pylib/anki/importing/supermemo_xml.py",
|
|
"./pylib/anki/statsbg.py",
|
|
"./pylib/tools/protoc-gen-mypy.py",
|
|
"./python/pyqt/install.py",
|
|
"./python/write_wheel.py",
|
|
"./qt/aqt/mpv.py",
|
|
"./qt/aqt/winpaths.py",
|
|
"./qt/bundle/build.rs",
|
|
"./qt/bundle/src/main.rs",
|
|
];
|
|
|
|
const IGNORED_FOLDERS: &[&str] = &[
|
|
"./out",
|
|
"./node_modules",
|
|
"./qt/aqt/forms",
|
|
"./tools/workspace-hack",
|
|
"./qt/bundle/PyOxidizer",
|
|
"./target",
|
|
];
|
|
|
|
fn main() -> Result<()> {
|
|
let want_fix = env::args().nth(1) == Some("fix".to_string());
|
|
let mut ctx = LintContext::new(want_fix);
|
|
ctx.check_contributors()?;
|
|
ctx.walk_folders(Path::new("."))?;
|
|
if ctx.found_problems {
|
|
std::process::exit(1);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
struct LintContext {
|
|
want_fix: bool,
|
|
unstaged_changes: Lazy<()>,
|
|
found_problems: bool,
|
|
nonstandard_headers: HashSet<&'static Utf8Path>,
|
|
}
|
|
|
|
impl LintContext {
|
|
pub fn new(want_fix: bool) -> Self {
|
|
Self {
|
|
want_fix,
|
|
unstaged_changes: Lazy::new(check_for_unstaged_changes),
|
|
found_problems: false,
|
|
nonstandard_headers: NONSTANDARD_HEADER.iter().map(Utf8Path::new).collect(),
|
|
}
|
|
}
|
|
|
|
pub fn walk_folders(&mut self, root: &Path) -> Result<()> {
|
|
let ignored_folders: HashSet<_> = IGNORED_FOLDERS.iter().map(Utf8Path::new).collect();
|
|
let walker = WalkDir::new(root).into_iter();
|
|
for entry in walker.filter_entry(|e| {
|
|
!ignored_folders.contains(&Utf8Path::from_path(e.path()).expect("utf8"))
|
|
}) {
|
|
let entry = entry.unwrap();
|
|
let path = Utf8Path::from_path(entry.path()).context("utf8")?;
|
|
|
|
let exts: HashSet<_> = ["py", "ts", "rs", "svelte", "mjs"]
|
|
.into_iter()
|
|
.map(Some)
|
|
.collect();
|
|
if exts.contains(&path.extension()) {
|
|
self.check_copyright(path)?;
|
|
self.check_triple_slash(path)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn check_copyright(&mut self, path: &Utf8Path) -> Result<()> {
|
|
if path.file_name().unwrap().ends_with(".d.ts") {
|
|
return Ok(());
|
|
}
|
|
let head = head_of_file(path)?;
|
|
if head.is_empty() {
|
|
return Ok(());
|
|
}
|
|
if self.nonstandard_headers.contains(&path) {
|
|
return Ok(());
|
|
}
|
|
let missing = !head.contains("Ankitects Pty Ltd and contributors");
|
|
if missing {
|
|
if self.want_fix {
|
|
Lazy::force(&self.unstaged_changes);
|
|
fix_copyright(path)?;
|
|
} else {
|
|
println!("missing standard copyright header: {:?}", path);
|
|
self.found_problems = true;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn check_triple_slash(&mut self, path: &Utf8Path) -> Result<()> {
|
|
if !matches!(path.extension(), Some("ts") | Some("svelte")) {
|
|
return Ok(());
|
|
}
|
|
for line in fs::read_to_string(path)?.lines() {
|
|
if line.contains("///") && !line.contains("/// <reference") {
|
|
println!("not a docstring: {path}: {line}");
|
|
self.found_problems = true;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn check_contributors(&self) -> Result<()> {
|
|
let antispam = ", at the domain ";
|
|
|
|
let last_author = String::from_utf8(
|
|
Command::new("git")
|
|
.args(["log", "-1", "--pretty=format:%ae"])
|
|
.output()?
|
|
.stdout,
|
|
)?;
|
|
|
|
let all_contributors = String::from_utf8(
|
|
Command::new("git")
|
|
.args(["log", "--pretty=format:%ae", "CONTRIBUTORS"])
|
|
.output()?
|
|
.stdout,
|
|
)?;
|
|
let all_contributors = all_contributors.lines().collect::<HashSet<&str>>();
|
|
|
|
if last_author == "49699333+dependabot[bot]@users.noreply.github.com" {
|
|
println!("Dependabot whitelisted.");
|
|
return Ok(());
|
|
} else if all_contributors.contains(last_author.as_str()) {
|
|
println!("Author found in CONTRIBUTORS");
|
|
return Ok(());
|
|
}
|
|
|
|
println!("All contributors:");
|
|
println!("{}", {
|
|
let mut contribs: Vec<_> = all_contributors
|
|
.iter()
|
|
.map(|s| s.replace('@', antispam))
|
|
.collect();
|
|
contribs.sort();
|
|
contribs.join("\n")
|
|
});
|
|
|
|
println!(
|
|
"Author {} NOT found in list",
|
|
last_author.replace('@', antispam)
|
|
);
|
|
|
|
println!(
|
|
"\nPlease make sure you modify the CONTRIBUTORS file using the email address you \
|
|
are committing from. If you have GitHub configured to hide your email address, \
|
|
you may need to make a change to the CONTRIBUTORS file using the GitHub UI, \
|
|
then try again."
|
|
);
|
|
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
fn head_of_file(path: &Utf8Path) -> Result<String> {
|
|
let mut file = File::open(path)?;
|
|
let mut buffer = vec![0; 256];
|
|
let size = file.read(&mut buffer)?;
|
|
buffer.truncate(size);
|
|
Ok(String::from_utf8(buffer).unwrap_or_default())
|
|
}
|
|
|
|
fn fix_copyright(path: &Utf8Path) -> Result<()> {
|
|
let header = match path.extension().unwrap() {
|
|
"py" => {
|
|
r#"# Copyright: Ankitects Pty Ltd and contributors
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
"#
|
|
}
|
|
"ts" | "rs" | "mjs" => {
|
|
r#"// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
"#
|
|
}
|
|
"svelte" => {
|
|
r#"<!--
|
|
Copyright: Ankitects Pty Ltd and contributors
|
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
-->
|
|
"#
|
|
}
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
let data = fs::read_to_string(path).with_context(|| format!("reading {path}"))?;
|
|
let mut file = fs::OpenOptions::new()
|
|
.write(true)
|
|
.open(path)
|
|
.with_context(|| format!("opening {path}"))?;
|
|
write!(file, "{}{}", header, data).with_context(|| format!("writing {path}"))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn check_for_unstaged_changes() {
|
|
let output = Command::new("git").arg("diff").output().unwrap();
|
|
if !output.stdout.is_empty() {
|
|
println!("stage any changes first");
|
|
std::process::exit(1);
|
|
}
|
|
}
|