1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
//! The seam through which tool knowledge (plugins) feeds the graph.
//!
//! The graph never knows which tools a repository uses. A host registers any number of [`InputSource`]s; for every
//! workspace package each one returns a [`WorkspaceInputs`] with the entry and project globs, ignore lists and
//! references the tool implies. The plugin table of `skif-js-plugins` implements this trait.
use std::path::PathBuf;
use skif_core::FileSystem;
pub use skif_core::{IncompleteCause, IncompleteEntries};
use skif_workspace::{Package, Workspace};
/// A module alias a tool configuration declares: imports of `find` (and `find/…`) point at `target`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathAlias {
/// The aliased prefix, without a trailing `/` or `*`.
pub find: String,
/// The directory or file it stands for, relative to the package directory with `/` separators. `None` when the
/// configuration computes it, in which case imports of the alias are known not to be packages but cannot be
/// followed.
pub target: Option<String>,
}
/// Build output that maps back to source: `dist` to `src` for a `tsconfig` with `outDir` and `rootDir`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BuildDir {
/// The output directory, relative to the package directory.
pub out_dir: String,
/// The source directory, relative to the package directory.
pub root_dir: String,
}
/// A custom element library: a class it registers is used by the browser through its tag name, so it is no unused
/// export (ADR 0062). `customElements.define('x-y', Class)` counts without one.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomElementLibrary {
/// Specifiers the library is imported from; one that ends with `/` matches every specifier below it.
pub modules: Vec<String>,
/// The name of the registering decorator as the library exports it (`customElement`).
pub decorator: String,
/// The base class whose subclasses register with a static `define(…)` (`FASTElement`), when there is one.
pub base_class: Option<String>,
}
impl CustomElementLibrary {
/// Whether `specifier` imports this library.
#[must_use]
pub fn is_module(&self, specifier: &str) -> bool {
self.modules
.iter()
.any(|module| module == specifier || (module.ends_with('/') && specifier.starts_with(module.as_str())))
}
}
/// What an [`InputSource`] contributes for one workspace package.
///
/// Globs are relative to the package directory and use `/` separators. A leading `!` excludes.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct WorkspaceInputs {
/// Entry files in every mode, such as test files or framework routes.
pub entry: Vec<String>,
/// Entry files that are part of the shipped product; the only plugin entries used in production mode.
pub production_entry: Vec<String>,
/// Files that belong to the project, added to the default or configured project globs.
pub project: Vec<String>,
/// Files whose issues are never reported, relative to the package directory.
pub ignore: Vec<String>,
/// Package names (globs allowed) that are never reported as unused, unlisted or optional peers.
pub ignore_dependencies: Vec<String>,
/// Binary names (globs allowed) that are never reported as unlisted.
pub ignore_binaries: Vec<String>,
/// Specifier patterns (globs allowed) the tool resolves at build time, such as `@generated/*` or `#imports`. They
/// are no dependencies and no file backs them, so they are never reported as unlisted or unresolved.
pub virtual_modules: Vec<String>,
/// Packages the tool uses without importing them, such as an `ESLint` parser named in a config.
pub dependencies: Vec<String>,
/// Binaries the tool runs without a script naming them.
pub binaries: Vec<String>,
/// Configuration files of the tools, absolute. They are entries in every mode so what they import counts.
pub config_files: Vec<PathBuf>,
/// Module aliases declared by bundler and compiler configuration.
pub aliases: Vec<PathAlias>,
/// Output directories and the source directories they are built from.
pub build_dirs: Vec<BuildDir>,
/// Extra `exports` conditions that select source files (`customConditions`).
pub conditions: Vec<String>,
/// Directories bare specifiers are looked up in (`baseUrl`, `paths` wildcards, `resolve.modules`), relative to the
/// package directory with `/` separators, in lookup order.
pub module_dirs: Vec<String>,
/// File extensions (with the leading dot) imports may omit, from bundler `resolve.extensions`.
pub extensions: Vec<String>,
/// Custom element libraries the package uses, whose registered classes are used exports.
pub custom_elements: Vec<CustomElementLibrary>,
/// Whether a tool's entries could only be partly read (a computed `entry`), so unused files and dependencies of
/// the package cannot be told apart from ones reached through the unknown entries.
pub entries_incomplete: bool,
/// Why the entries are incomplete, when `entries_incomplete` is set and the source knows.
pub incomplete_reasons: Vec<IncompleteEntries>,
/// Where the entries the source could not read can lie, when `entries_incomplete` is set: only files there are
/// left unjudged by `unused-files` (ADR 0065).
pub incomplete_scope: IncompleteScope,
}
/// Where the unknown entries of a package can lie; see [`WorkspaceInputs::incomplete_scope`].
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum IncompleteScope {
/// Anywhere: no file of the package is judged unused.
#[default]
Package,
/// Only files matching these globs, relative to the package directory, may be unknown entries; they are treated
/// as entries and every other file is judged as usual.
Within(Vec<String>),
}
impl IncompleteScope {
/// The scope covering both `self` and `other`: the whole package when either is.
#[must_use]
pub fn union(self, other: Self) -> Self {
match (self, other) {
(Self::Within(mut globs), Self::Within(more)) => {
for glob in more {
if !globs.contains(&glob) {
globs.push(glob);
}
}
Self::Within(globs)
}
_ => Self::Package,
}
}
}
impl WorkspaceInputs {
/// Appends everything in `other`.
pub fn extend(&mut self, other: Self) {
self.entry.extend(other.entry);
self.production_entry.extend(other.production_entry);
self.project.extend(other.project);
self.ignore.extend(other.ignore);
self.ignore_dependencies.extend(other.ignore_dependencies);
self.ignore_binaries.extend(other.ignore_binaries);
self.virtual_modules.extend(other.virtual_modules);
self.dependencies.extend(other.dependencies);
self.binaries.extend(other.binaries);
self.config_files.extend(other.config_files);
self.aliases.extend(other.aliases);
self.build_dirs.extend(other.build_dirs);
self.conditions.extend(other.conditions);
self.module_dirs.extend(other.module_dirs);
self.extensions.extend(other.extensions);
self.custom_elements.extend(other.custom_elements);
if other.entries_incomplete {
self.incomplete_scope = if self.entries_incomplete {
std::mem::take(&mut self.incomplete_scope).union(other.incomplete_scope)
} else {
other.incomplete_scope
};
}
self.entries_incomplete |= other.entries_incomplete;
self.incomplete_reasons.extend(other.incomplete_reasons);
}
}
/// The package an [`InputSource`] is asked about.
#[derive(Clone, Copy)]
pub struct InputRequest<'a> {
/// The whole workspace.
pub workspace: &'a Workspace,
/// The package whose inputs are wanted.
pub package: &'a Package,
/// File access, for reading tool configuration files.
pub fs: &'a dyn FileSystem,
/// The dependencies listed by the `package.json` files above the workspace root (a nested root sees the tools
/// of the project that contains it), sorted and without duplicates.
pub ancestor_dependencies: &'a [String],
}
impl std::fmt::Debug for InputRequest<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InputRequest")
.field("package", &self.package.display_name())
.finish_non_exhaustive()
}
}
/// A provider of tool-specific entries, project files, ignores and references.
pub trait InputSource: Send + Sync {
/// The inputs for `request.package`; an empty value when the tool is not used there.
fn inputs(&self, request: &InputRequest<'_>) -> WorkspaceInputs;
}
const _: () = {
fn assert_send_sync<T: Send + Sync>() {}
let _ = assert_send_sync::<WorkspaceInputs>;
};