Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "typst-package-check"
version = "0.4.6"
edition = "2021"
edition = "2024"

[dependencies]
casbab = "0.1.1"
Expand Down
5 changes: 3 additions & 2 deletions src/action.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::{
check::TryExt,
github::{
api::{pr::PullRequestEvent, GitHubAuth, Installation, Repository},
run_github_check, AppState,
AppState,
api::{GitHubAuth, Installation, Repository, pr::PullRequestEvent},
run_github_check,
},
};

Expand Down
3 changes: 2 additions & 1 deletion src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use std::path::PathBuf;

use codespan_reporting::diagnostic::Label;
use typst::{
syntax::{package::PackageSpec, FileId, Span},
WorldExt,
syntax::{FileId, Span, package::PackageSpec},
};

use crate::world::SystemWorld;
Expand All @@ -19,6 +19,7 @@ mod path;
mod readme;

pub use diagnostics::{Diagnostics, Result, TryExt};
pub use manifest::Exclude;

pub async fn all_checks(
package_spec: Option<&PackageSpec>,
Expand Down
2 changes: 1 addition & 1 deletion src/check/authors.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use codespan_reporting::diagnostic::{Diagnostic, Label};
use typst::syntax::{package::PackageSpec, FileId, VirtualPath};
use typst::syntax::{FileId, VirtualPath, package::PackageSpec};

use crate::{github::git, package::PackageExt};

Expand Down
2 changes: 1 addition & 1 deletion src/check/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use typst::{

use crate::world::SystemWorld;

use super::{label, Diagnostics};
use super::{Diagnostics, label};

pub fn check(diags: &mut Diagnostics, world: &SystemWorld) -> Option<PagedDocument> {
let result = typst::compile(world);
Expand Down
127 changes: 90 additions & 37 deletions src/check/files.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
use std::collections::HashSet;
use std::ffi::OsStr;
use std::path::Path;

use codespan_reporting::diagnostic::{Diagnostic, Label};
use walkdir::WalkDir;

use crate::check::Diagnostics;
use crate::check::manifest::Manifest;
use crate::check::path::PackagePath;
use crate::check::readme::Readme;
use crate::check::Diagnostics;

/// Creates a directory iterator with the same settings as the package bundler.
pub fn walk(dir: &Path) -> ignore::Walk {
ignore::WalkBuilder::new(dir)
.sort_by_file_name(|a, b| a.cmp(b))
// Disable non-local ignore features
.parents(false)
.require_git(false)
.git_global(false)
.git_exclude(false)
// Keep local ignore features for now.
.git_ignore(true)
.ignore(true)
.hidden(true)
.build()
}

pub fn check(
diags: &mut Diagnostics,
Expand All @@ -15,52 +33,85 @@ pub fn check(
readme: &Option<Readme>,
) {
let exclude = &manifest.package.exclude;
let thumbnail_path = manifest.thumbnail();

// Manually keep track of excluded directories, to figure out if nested
// files are ignored. This is done, so we can generate diagnostics for
// excluded files.
let mut excluded_dirs = HashSet::new();
// The bundler enables some local ignore features, which can be confusing.
// Collect the list of files the bundler would consider bundling, without
// the exclude globs. Use it to determine if a file is ignored.
let without_ignored = walk(package_dir)
.flatten()
.filter_map(|ch| {
let metadata = ch.metadata().ok()?;
metadata.is_file().then_some(ch.into_path())
})
.collect::<HashSet<_>>();

for ch in ignore::WalkBuilder::new(package_dir).hidden(false).build() {
let Ok(ch) = ch else { continue };
for ch in WalkDir::new(package_dir).into_iter().flatten() {
let Ok(metadata) = ch.metadata() else {
continue;
};

let file_path = PackagePath::from_full(package_dir, ch.path());

if metadata.is_dir() {
// If the parent directory is ignored, all children are ignored too.
if parent_is_excluded(&excluded_dirs, file_path)
|| exclude.matched(file_path.relative(), true).is_ignore()
{
excluded_dirs.insert(ch.into_path());
}
if !metadata.is_file() {
continue;
}

// The thumbnail is always excluded.
let is_thumbnail = thumbnail_path.is_some_and(|t| t.val == file_path);
let excluded = is_thumbnail
|| parent_is_excluded(&excluded_dirs, file_path)
|| exclude.matched(file_path.relative(), false).is_ignore();
let file_path = PackagePath::from_full(package_dir, ch.path());
let excluded = exclude.matches_file(&file_path);
let ignored = !without_ignored.contains(file_path.full());

warn_ignored_files(diags, file_path, excluded, ignored);
forbid_font_files(diags, file_path);
exclude_large_files(diags, file_path, excluded, metadata.len());
exclude_examples_and_tests(diags, file_path, excluded);
link_manuals(diags, readme, file_path, excluded);
}
}

fn parent_is_excluded(
excluded_dirs: &HashSet<std::path::PathBuf>,
fn warn_ignored_files(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the file is used in the README, but intentionally excluded from the bundle? In other words, what if the package developer uses .ignore as an alternative to exclude in the manifest?
I think adding a hint saying something like "If this is intentional, add this file to the exclude list in your manifest." could fix the problem and force package authors to be explicit about what they intend to exclude.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I've expanded the warning message a bit.

diags: &mut Diagnostics,
file_path: PackagePath<&Path>,
) -> bool {
file_path
.full()
.parent()
.is_some_and(|parent| excluded_dirs.contains(parent))
excluded: bool,
ignored: bool,
) {
if excluded || !ignored {
return;
}

// Don't emit noisy warnings for common hidden files that won't be a problem
// when missing from the bundle.
const COMMON: [&str; 5] = [
".gitattributes",
".gitignore",
".gitkeep",
".ignore",
".keep",
];
if COMMON.map(OsStr::new).contains(&file_path.file_name()) {
return;
}

let (reason, hint) = if file_path.file_name().as_encoded_bytes().starts_with(b".") {
(
".\nIt's ignored, because it is hidden: the file name starts with a `.`.",
"If not, consider removing the file.",
)
} else {
(
" because of an ignore file, such as `.gitignore` or `.ignore`.",
"If not, consider removing it or updating the ignore file.",
)
};

diags.emit(
Diagnostic::warning()
.with_code("files/ignored")
.with_label(Label::primary(file_path.file_id(), 0..0))
.with_message(format_args!(
"This file won't be present in the bundled package{reason}\n\n\
If this is intentional and the file is used for documentation and linked in the readme, \
consider explicitly adding it to the `exclude` list.\n\
{hint}\n\n\
More details: https://github.com/typst/packages/blob/main/docs/tips.md#what-to-commit-what-to-exclude",
)),
);
}

fn exclude_large_files(
Expand Down Expand Up @@ -218,13 +269,15 @@ fn link_manuals(
let note = (!excluded)
.then(|| "It should also be added to `exclude` in your `typst.toml`.".into());

diags.emit(Diagnostic::warning()
.with_label(Label::primary(path.file_id(), 0..0))
.with_code("files/manual/unlinked")
.with_message(
"This file seems to be a manual/documentation, but isn't linked in the readme. \
diags.emit(
Diagnostic::warning()
.with_label(Label::primary(path.file_id(), 0..0))
.with_code("files/manual/unlinked")
.with_message(
"This file seems to be a manual/documentation, but isn't linked in the readme. \
It will be inacessible on Typst Universe.",
)
.with_notes_iter(note));
)
.with_notes_iter(note),
);
}
}
33 changes: 14 additions & 19 deletions src/check/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ use std::{

use codespan_reporting::diagnostic::Diagnostic;
use typst::{
World,
syntax::{
ast::{self, AstNode, ModuleImport},
package::{PackageSpec, PackageVersion, VersionlessPackageSpec},
},
World,
};
use walkdir::WalkDir;

use crate::check::path::PackagePath;
use crate::check::{label, Diagnostics, Result, TryExt};
use crate::check::{Diagnostics, Result, TryExt, label};
use crate::world::SystemWorld;

pub fn check(diags: &mut Diagnostics, package_dir: &Path, world: &SystemWorld) -> Result<()> {
Expand Down Expand Up @@ -85,23 +85,18 @@ pub fn check_ast(
)
}

if let Some(all_packages) = all_packages {
if let Ok(import_spec) = PackageSpec::from_str(source_str.get().as_str()) {
if let Some(latest_version) =
latest_package_version(all_packages, import_spec.versionless())
{
if latest_version != import_spec.version {
diags.emit(
Diagnostic::warning()
.with_labels(label(world, import.span()).into_iter().collect())
.with_code("import/outdated")
.with_message(
"This import seems to use an older version of the package.",
),
)
}
}
}
if let Some(all_packages) = all_packages
&& let Ok(import_spec) = PackageSpec::from_str(source_str.get().as_str())
&& let Some(latest_version) =
latest_package_version(all_packages, import_spec.versionless())
&& latest_version != import_spec.version
{
diags.emit(
Diagnostic::warning()
.with_labels(label(world, import.span()).into_iter().collect())
.with_code("import/outdated")
.with_message("This import seems to use an older version of the package."),
)
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/check/kebab_case.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ use std::collections::HashSet;
use codespan_reporting::diagnostic::{Diagnostic, Severity};
use comemo::Track;
use typst::{
ROUTINES, World,
engine::{Route, Sink, Traced},
syntax::{
ast::{self, AstNode},
FileId, Source, SyntaxNode,
ast::{self, AstNode},
},
World, ROUTINES,
};

use crate::world::SystemWorld;

use super::{label, Diagnostics};
use super::{Diagnostics, label};

// Check that all public identifiers are in kebab-case
pub fn check(diags: &mut Diagnostics, world: &SystemWorld) -> Option<()> {
Expand Down
Loading
Loading