skif_js_syntax/model.rs
1//! Skif-owned description of one module. No oxc type appears here.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use skif_core::Span;
7use skif_core::{HashMap, HashSet};
8
9/// How a module is pulled in.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "kebab-case")]
12pub enum ImportKind {
13 /// `import a from "x"` (not type-only, has bindings).
14 Static,
15 /// `import type … from "x"`: the whole declaration is type-only.
16 TypeOnly,
17 /// `import "x"`.
18 SideEffect,
19 /// `import("x")` with a literal specifier.
20 Dynamic,
21 /// `require("x")`, `require.resolve("x")`, `import.meta.resolve("x")` and `import x = require("x")`.
22 Require,
23 /// One pattern of `import.meta.glob(...)`, or the glob of a computed relative `require` or `import()`
24 /// (`./commands/${name}`): a glob, not a module specifier.
25 ImportMetaGlob,
26 /// One pattern of an `import.meta.glob(...)` whose options read the matched files as text (`query: '?raw'`,
27 /// legacy `as: 'raw'`): the files are not loaded as modules, so the glob keeps none of them in use.
28 SourceGlob,
29 /// `export … from "x"`; the matching [`Export`] is recorded too.
30 ExportFrom,
31 /// `vi.mock("x")`, `jest.mock("x")` and the other test-runner calls that name a module: the test loads or
32 /// replaces the module, which keeps a local file reachable but is not an import of a dependency.
33 Mock,
34 /// `new URL("./x", import.meta.url)`, which the parser does not report: the file is loaded as a program of its own
35 /// (a worker, an asset), so every export of it counts as used, as for an entry.
36 Url,
37}
38
39/// A test-runner call that names a module, recorded on an [`ImportKind::Mock`] import. It serializes as the call is
40/// written (`"vi.mock"`).
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
42#[non_exhaustive]
43pub enum MockCall {
44 /// `vi.mock("x")`.
45 #[serde(rename = "vi.mock")]
46 ViMock,
47 /// `vi.doMock("x")`.
48 #[serde(rename = "vi.doMock")]
49 ViDoMock,
50 /// `vi.unmock("x")`.
51 #[serde(rename = "vi.unmock")]
52 ViUnmock,
53 /// `vi.doUnmock("x")`.
54 #[serde(rename = "vi.doUnmock")]
55 ViDoUnmock,
56 /// `vi.importActual("x")`.
57 #[serde(rename = "vi.importActual")]
58 ViImportActual,
59 /// `vi.importMock("x")`.
60 #[serde(rename = "vi.importMock")]
61 ViImportMock,
62 /// `jest.mock("x")`.
63 #[serde(rename = "jest.mock")]
64 JestMock,
65 /// `jest.doMock("x")`.
66 #[serde(rename = "jest.doMock")]
67 JestDoMock,
68 /// `jest.unmock("x")`.
69 #[serde(rename = "jest.unmock")]
70 JestUnmock,
71 /// `jest.dontMock("x")`.
72 #[serde(rename = "jest.dontMock")]
73 JestDontMock,
74 /// `jest.requireActual("x")`.
75 #[serde(rename = "jest.requireActual")]
76 JestRequireActual,
77 /// `jest.requireMock("x")`.
78 #[serde(rename = "jest.requireMock")]
79 JestRequireMock,
80 /// `jest.createMockFromModule("x")`.
81 #[serde(rename = "jest.createMockFromModule")]
82 JestCreateMockFromModule,
83 /// `jest.genMockFromModule("x")`.
84 #[serde(rename = "jest.genMockFromModule")]
85 JestGenMockFromModule,
86 /// `rs.mock("x")` (also written `rstest.mock`).
87 #[serde(rename = "rs.mock")]
88 RsMock,
89 /// `rs.mockRequire("x")` (also written `rstest.mockRequire`).
90 #[serde(rename = "rs.mockRequire")]
91 RsMockRequire,
92 /// `rs.doMock("x")` (also written `rstest.doMock`).
93 #[serde(rename = "rs.doMock")]
94 RsDoMock,
95 /// `rs.doMockRequire("x")` (also written `rstest.doMockRequire`).
96 #[serde(rename = "rs.doMockRequire")]
97 RsDoMockRequire,
98 /// `rs.unmock("x")` (also written `rstest.unmock`).
99 #[serde(rename = "rs.unmock")]
100 RsUnmock,
101 /// `rs.doUnmock("x")` (also written `rstest.doUnmock`).
102 #[serde(rename = "rs.doUnmock")]
103 RsDoUnmock,
104 /// `rs.unmockRequire("x")` (also written `rstest.unmockRequire`).
105 #[serde(rename = "rs.unmockRequire")]
106 RsUnmockRequire,
107 /// `rs.doUnmockRequire("x")` (also written `rstest.doUnmockRequire`).
108 #[serde(rename = "rs.doUnmockRequire")]
109 RsDoUnmockRequire,
110 /// `rs.importActual("x")` (also written `rstest.importActual`).
111 #[serde(rename = "rs.importActual")]
112 RsImportActual,
113 /// `rs.importMock("x")` (also written `rstest.importMock`).
114 #[serde(rename = "rs.importMock")]
115 RsImportMock,
116 /// `rs.requireActual("x")` (also written `rstest.requireActual`).
117 #[serde(rename = "rs.requireActual")]
118 RsRequireActual,
119 /// `rs.requireMock("x")` (also written `rstest.requireMock`).
120 #[serde(rename = "rs.requireMock")]
121 RsRequireMock,
122}
123
124impl MockCall {
125 /// Every call: the `vi` ones, then the `jest` ones, then rstest's (<https://rstest.rs/api/runtime-api/rstest/mock-modules>).
126 pub const ALL: [Self; 26] = [
127 Self::ViMock,
128 Self::ViDoMock,
129 Self::ViUnmock,
130 Self::ViDoUnmock,
131 Self::ViImportActual,
132 Self::ViImportMock,
133 Self::JestMock,
134 Self::JestDoMock,
135 Self::JestUnmock,
136 Self::JestDontMock,
137 Self::JestRequireActual,
138 Self::JestRequireMock,
139 Self::JestCreateMockFromModule,
140 Self::JestGenMockFromModule,
141 Self::RsMock,
142 Self::RsMockRequire,
143 Self::RsDoMock,
144 Self::RsDoMockRequire,
145 Self::RsUnmock,
146 Self::RsDoUnmock,
147 Self::RsUnmockRequire,
148 Self::RsDoUnmockRequire,
149 Self::RsImportActual,
150 Self::RsImportMock,
151 Self::RsRequireActual,
152 Self::RsRequireMock,
153 ];
154
155 /// The call as written: `vi.mock`, `jest.requireActual`.
156 #[must_use]
157 pub const fn as_str(self) -> &'static str {
158 match self {
159 Self::ViMock => "vi.mock",
160 Self::ViDoMock => "vi.doMock",
161 Self::ViUnmock => "vi.unmock",
162 Self::ViDoUnmock => "vi.doUnmock",
163 Self::ViImportActual => "vi.importActual",
164 Self::ViImportMock => "vi.importMock",
165 Self::JestMock => "jest.mock",
166 Self::JestDoMock => "jest.doMock",
167 Self::JestUnmock => "jest.unmock",
168 Self::JestDontMock => "jest.dontMock",
169 Self::JestRequireActual => "jest.requireActual",
170 Self::JestRequireMock => "jest.requireMock",
171 Self::JestCreateMockFromModule => "jest.createMockFromModule",
172 Self::JestGenMockFromModule => "jest.genMockFromModule",
173 Self::RsMock => "rs.mock",
174 Self::RsMockRequire => "rs.mockRequire",
175 Self::RsDoMock => "rs.doMock",
176 Self::RsDoMockRequire => "rs.doMockRequire",
177 Self::RsUnmock => "rs.unmock",
178 Self::RsDoUnmock => "rs.doUnmock",
179 Self::RsUnmockRequire => "rs.unmockRequire",
180 Self::RsDoUnmockRequire => "rs.doUnmockRequire",
181 Self::RsImportActual => "rs.importActual",
182 Self::RsImportMock => "rs.importMock",
183 Self::RsRequireActual => "rs.requireActual",
184 Self::RsRequireMock => "rs.requireMock",
185 }
186 }
187
188 /// The call `object.property`, when it is one. `rstest` is an alias of `rs`.
189 #[must_use]
190 pub fn find(object: &str, property: &str) -> Option<Self> {
191 let object = if object == "rstest" { "rs" } else { object };
192 Self::ALL
193 .into_iter()
194 .find(|call| call.as_str().split_once('.') == Some((object, property)))
195 }
196}
197
198impl std::fmt::Display for MockCall {
199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 f.write_str(self.as_str())
201 }
202}
203
204/// Which export of the target module a binding refers to.
205#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
206#[serde(rename_all = "kebab-case")]
207pub enum ImportedKind {
208 /// The default export.
209 Default,
210 /// The whole namespace (`import * as ns`, `export * from`, `require("x")`, `import("x")`).
211 Namespace,
212 /// A named export.
213 Named(String),
214}
215
216/// One binding brought in by an [`Import`].
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
218#[serde(rename_all = "kebab-case")]
219pub struct ImportedName {
220 /// What is imported.
221 pub kind: ImportedKind,
222 /// Local binding name; `None` for re-exports, side effects and unbound `require`/`import()`.
223 pub local: Option<String>,
224 /// `import { type A }` or a type-only declaration.
225 pub is_type: bool,
226 /// Members read through the local binding in this file (`ns.a`, `Enum.A`, `const { a } = ns`, `ns.A` in types).
227 /// Sorted and deduplicated. Meaningful only when `local` is set.
228 pub accessed_members: Vec<String>,
229 /// Members read one level further down, by the member of [`ImportedName::accessed_members`] they are read
230 /// through: `ns.E.A`, `ns.E['A']` and the type `ns.E.A` read `A` of `ns.E`. A member has an entry only when every
231 /// use of it reads its own members that way; a member without one is used as a whole (`ns.E` passed around,
232 /// `typeof ns.E`). Each list is sorted and deduplicated. Meaningful only when `local` is set.
233 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
234 pub nested_members: BTreeMap<String, Vec<String>>,
235 /// The binding is used in a way that cannot be reduced to members (passed around, re-exported, computed access,
236 /// destructured with a rest element), or the module value is not bound at all (`import("x")`, bare `require("x")`).
237 pub escapes: bool,
238}
239
240/// A module reference.
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242#[serde(rename_all = "kebab-case")]
243pub struct Import {
244 /// The specifier as written (a glob pattern for [`ImportKind::ImportMetaGlob`]).
245 pub specifier: String,
246 /// How it is referenced.
247 pub kind: ImportKind,
248 /// Bindings it brings in.
249 pub names: Vec<ImportedName>,
250 /// The test-runner call of an [`ImportKind::Mock`] (`vi.mock`, `jest.requireActual`); `None` for every other kind.
251 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub call: Option<MockCall>,
253 /// Location of the whole statement or call.
254 pub span: Span,
255}
256
257/// What an export is.
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
259#[serde(rename_all = "kebab-case")]
260pub enum ExportKind {
261 /// `const`, `let`, `var`, and `export { x }` of anything not recognised below.
262 Value,
263 /// `interface` and `type` aliases.
264 Type,
265 /// `enum` and `const enum`.
266 Enum,
267 /// TypeScript `namespace`/`module`.
268 Namespace,
269 /// `class`.
270 Class,
271 /// `function`.
272 Function,
273 /// `export default …` (named `default`).
274 Default,
275 /// `export { a as b } from "x"`.
276 ReExport,
277 /// `export * from "x"` (named `*`) and `export * as ns from "x"` (named `ns`).
278 StarReExport,
279}
280
281/// A member of an enum or TypeScript namespace.
282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
283#[serde(rename_all = "kebab-case")]
284pub struct Member {
285 /// Member name.
286 pub name: String,
287 /// Location of the member name.
288 pub span: Span,
289 /// `JSDoc` tag names (without `@`) on the member.
290 pub tags: Vec<String>,
291 /// The byte range (absolute, end exclusive) that deletes the member: its line when it stands alone on it, else
292 /// the member with its comma and the blanks after it. `None` when deleting it would break the file: another
293 /// member mentions its name, or one statement declares it with siblings.
294 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub removal: Option<(u32, u32)>,
296}
297
298/// What a removal edit deletes.
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
300#[serde(rename_all = "kebab-case")]
301pub enum RemovalKind {
302 /// Only the `export` (or `export default`) keyword and following whitespace: the declaration stays.
303 ExportKeyword,
304 /// Like [`RemovalKind::ExportKeyword`], but the keyword is shared by several exports of one statement
305 /// (`export const a = 1, b = 2`); apply it only when every sibling is unused.
306 SharedExportKeyword,
307 /// One specifier with its separating comma. Ranges of adjacent specifiers of one statement overlap; use
308 /// [`ModuleInfo::removal_edits`] (or [`coalesce_ranges`]) to get disjoint edits.
309 Specifier,
310 /// The whole statement, including one trailing line break.
311 Statement,
312}
313
314/// A byte range to delete to fix an unused export.
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
316#[serde(rename_all = "kebab-case")]
317pub struct Removal {
318 /// Start byte offset.
319 pub start: u32,
320 /// End byte offset (exclusive).
321 pub end: u32,
322 /// What the range deletes.
323 pub kind: RemovalKind,
324 /// Byte range of the whole export statement including one trailing line break.
325 pub statement: (u32, u32),
326 /// Number of exports declared by that statement (including this one).
327 pub siblings: u32,
328 /// The statement is `export default <expression>` (or an anonymous default declaration) and
329 /// [`RemovalKind::Statement`] deletes the expression too, not only the export. Only an expression whose evaluation
330 /// has no effect gets a removal (a literal, an identifier, a function, a class that runs no code, or an array or
331 /// object of those); callers that want a conservative fix can still skip or flag these.
332 pub default_expression: bool,
333}
334
335/// One export of a module.
336#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
337#[serde(rename_all = "kebab-case")]
338pub struct Export {
339 /// Exported name: `default`, `*` for `export * from`, otherwise the name importers use.
340 pub name: String,
341 /// What it is.
342 pub kind: ExportKind,
343 /// Exists only at type level (`interface`, `type`, `export type { … }`).
344 pub is_type: bool,
345 /// Local binding behind the export, when it has one.
346 pub local_name: Option<String>,
347 /// Source module of a re-export.
348 pub from: Option<String>,
349 /// Name taken from the source module of a re-export (`*` for a star re-export).
350 pub imported_name: Option<String>,
351 /// `JSDoc` tag names (without `@`) in the comment directly above, such as `public`, `internal`, `alias`.
352 pub tags: Vec<String>,
353 /// Location of the exported name (the statement for star re-exports).
354 pub span: Span,
355 /// Members of an enum export.
356 pub enum_members: Vec<Member>,
357 /// Exported members of a namespace export.
358 pub namespace_members: Vec<Member>,
359 /// How to remove the export, when a safe edit exists.
360 pub removal: Option<Removal>,
361 /// The local binding is referenced elsewhere in this file (excluding export clauses).
362 pub referenced_in_file: bool,
363 /// Names of the other value exports of this file whose signature mentions this export's local binding.
364 #[serde(default)]
365 pub referenced_in: Vec<String>,
366 /// Members of an enum or namespace export read in this file by name (`E.A`, `E['A']`, `const { A } = E`, or a
367 /// namespace member's bare name inside the namespace body); sorted.
368 #[serde(default)]
369 pub members_read_in_file: Vec<String>,
370 /// An enum export used as a whole in this file, which may read any member: as a value (`Object.values(E)`,
371 /// `E[key]`, a spread, `for (k in E)`, passed, stored or returned) or in a type through `typeof E` or a template
372 /// literal type (`${E}`). A type annotation `x: E` names the enum type and reads no member.
373 #[serde(default)]
374 pub used_whole_in_file: bool,
375 /// `export const b = a`: the identifier the exported constant is initialised with.
376 #[serde(default)]
377 pub alias_of: Option<String>,
378 /// How the class behind the export is registered as a custom element, in source order. A registered element is
379 /// used by the browser through its tag name, not through an import.
380 #[serde(default)]
381 pub registrations: Vec<Registration>,
382}
383
384/// How a class is registered as a custom element (ADR 0020).
385///
386/// [`Registration::Define`] is unambiguous on its own. The other two only name the binding the registration goes
387/// through; whether that binding registers anything depends on the library it comes from, which the caller decides
388/// (`lit/decorators.js` registers with `customElement`, `@stencil/core` with `Component`).
389#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
390#[serde(rename_all = "kebab-case")]
391pub enum Registration {
392 /// `customElements.define('x-y', Class)`, also through `window.customElements`, a variable holding the registry
393 /// (resolved by scope) or a `new CustomElementRegistry()`, and `this` in a static block of the class.
394 Define,
395 /// A decorator of a top-level class, `@name(…)`, `@name` or `@ns.name(…)`, whose binding is imported from
396 /// `module`.
397 Decorator {
398 /// The specifier the decorator is imported from.
399 module: String,
400 /// The imported name (the member name for a namespace import).
401 name: String,
402 },
403 /// A top-level class extends `name` imported from `module` (directly or as an argument of a mixin call,
404 /// `extends Mixin(Base)`), and `Class.define(…)` or `Class.defineAsync(…)` is called; or `name.define(Class, …)`, the
405 /// static form on the imported class, is called with the class first.
406 BaseClassDefine {
407 /// The specifier the base class is imported from.
408 module: String,
409 /// The imported name of the base class.
410 name: String,
411 },
412}
413
414/// A syntax problem reported by the parser. The analysis of the file continues on the parser's recovered program.
415#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
416#[serde(rename_all = "kebab-case")]
417pub struct ParseIssue {
418 /// The parser's message.
419 pub message: String,
420 /// Location of the problem in the original file.
421 pub span: Span,
422}
423
424/// Everything skif needs to know about one source file.
425#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
426#[serde(rename_all = "kebab-case")]
427pub struct ModuleInfo {
428 /// Module references, in source order.
429 pub imports: Vec<Import>,
430 /// Exports, in source order.
431 pub exports: Vec<Export>,
432 /// Export names referenced inside the same file; sorted, deduplicated.
433 pub in_file_references: Vec<String>,
434 /// `module.exports = …`, `module.exports.x = …` or `exports.x = …` anywhere in the file. The file is then a
435 /// `CommonJS` module whose export names are not tracked: treat all of its exports as used.
436 pub has_cjs_exports: bool,
437 /// The file contains JSX.
438 pub has_jsx: bool,
439 /// Value of a `@jsxImportSource` pragma.
440 pub jsx_import_source: Option<String>,
441 /// Location of the value of the `@jsxImportSource` pragma. Only a pragma in the comments before the first
442 /// statement counts.
443 pub jsx_import_source_span: Option<Span>,
444 /// Directive prologue strings of the top level, such as `use client` and `use server`. See
445 /// [`ModuleInfo::is_client_boundary`] and [`ModuleInfo::is_server_actions`].
446 pub directives: Vec<String>,
447 /// `import()`, `require()` and `import.meta.glob()` calls whose argument is not static.
448 pub unresolved_dynamic: Vec<Span>,
449 /// Names declared with `declare module 'name' { … }` in a script (a file without top-level `import` or
450 /// `export`): the declaration is the module, for every file of the program.
451 #[serde(default)]
452 pub ambient_modules: Vec<String>,
453 /// Names augmented with `declare module 'name' { … }` in a module file: the block adds to a module that has to
454 /// exist already and declares nothing on its own.
455 #[serde(default)]
456 pub augmented_modules: Vec<String>,
457 /// The command lines the file runs through `node:child_process` (`execSync('tsc -b')`, `spawn('eslint', ['.'])`),
458 /// in source order without duplicates; only literal commands. They are parsed like the scripts of `package.json`.
459 #[serde(default, skip_serializing_if = "Vec::is_empty")]
460 pub commands: Vec<String>,
461 /// The tag names of the template of a `.vue` file as written (`AppButton`, `app-card`, `div`), with the static
462 /// values of its `is` attributes (`is="AppButton"`, `:is="'AppButton'"`), and the names of the
463 /// `resolveComponent('AppButton')` calls of any file, sorted and without duplicates: `unplugin-vue-components`
464 /// imports the components it registers into the files that use them so (ADR 0020). Empty for other files.
465 #[serde(default, skip_serializing_if = "Vec::is_empty")]
466 pub template_tags: Vec<String>,
467 /// Syntax errors the parser recovered from; the other fields describe the recovered program. For component
468 /// files, the issues of every script block.
469 pub parse_diagnostics: Vec<ParseIssue>,
470 /// The parser could not recover (or, for a component file, could not recover in at least one script block): the
471 /// facts of the affected code are missing. Report the file as unparseable instead of treating it as empty.
472 pub fatal: bool,
473}
474
475impl ModuleInfo {
476 /// Removal edits of every export that has one, in source order.
477 pub fn removal_ranges(&self) -> impl Iterator<Item = &Removal> {
478 self.exports.iter().filter_map(|e| e.removal.as_ref())
479 }
480
481 /// The file starts with the `"use client"` directive: a React Server Components client boundary whose exports
482 /// are consumed by the framework.
483 #[must_use]
484 pub fn is_client_boundary(&self) -> bool {
485 self.directives.iter().any(|d| d == "use client")
486 }
487
488 /// The file starts with the `"use server"` directive: every export is a server action the framework calls.
489 #[must_use]
490 pub fn is_server_actions(&self) -> bool {
491 self.directives.iter().any(|d| d == "use server")
492 }
493
494 /// Disjoint, sorted deletions that remove every export for which `is_unused` holds.
495 ///
496 /// Specifier ranges of one statement are merged; a statement whose specifiers are all unused is deleted whole
497 /// instead of leaving `export {}`; a [`RemovalKind::SharedExportKeyword`] applies only when every sibling is
498 /// unused.
499 pub fn removal_edits(&self, is_unused: impl Fn(&Export) -> bool) -> Vec<(u32, u32)> {
500 let mut selected: HashMap<(u32, u32), u32> = HashMap::default();
501 for export in &self.exports {
502 if let Some(removal) = &export.removal
503 && is_unused(export)
504 {
505 *selected.entry(removal.statement).or_default() += 1;
506 }
507 }
508 let mut ranges = Vec::new();
509 let mut whole_statements = HashSet::default();
510 let mut clauses: HashMap<(u32, u32), Vec<(Span, bool)>> = HashMap::default();
511 // Every specifier of a clause, used or not: a run's separator depends on its neighbours.
512 for export in &self.exports {
513 if let Some(removal) = &export.removal
514 && removal.kind == RemovalKind::Specifier
515 {
516 clauses
517 .entry(removal.statement)
518 .or_default()
519 .push((export.span, is_unused(export)));
520 }
521 }
522 for export in &self.exports {
523 let Some(removal) = &export.removal else { continue };
524 if !is_unused(export) {
525 continue;
526 }
527 let all_selected = selected.get(&removal.statement).copied().unwrap_or(0) >= removal.siblings;
528 match removal.kind {
529 RemovalKind::ExportKeyword | RemovalKind::Statement => ranges.push((removal.start, removal.end)),
530 RemovalKind::SharedExportKeyword if all_selected => ranges.push((removal.start, removal.end)),
531 RemovalKind::Specifier if all_selected => {
532 whole_statements.insert(removal.statement);
533 }
534 // Partly unused shared declarations stay; partly unused clauses are handled per run below.
535 RemovalKind::SharedExportKeyword | RemovalKind::Specifier => {}
536 }
537 }
538 for (statement, specifiers) in &mut clauses {
539 if whole_statements.contains(statement) {
540 continue;
541 }
542 specifiers.sort_by_key(|(span, _)| span.start);
543 ranges.extend(specifier_runs(specifiers));
544 }
545 ranges.extend(whole_statements);
546 coalesce_ranges(ranges)
547 }
548
549 pub(crate) fn merge(&mut self, other: Self) {
550 self.imports.extend(other.imports);
551 self.exports.extend(other.exports);
552 self.in_file_references.extend(other.in_file_references);
553 self.in_file_references.sort();
554 self.in_file_references.dedup();
555 self.has_cjs_exports |= other.has_cjs_exports;
556 self.has_jsx |= other.has_jsx;
557 if self.jsx_import_source.is_none() {
558 self.jsx_import_source = other.jsx_import_source;
559 self.jsx_import_source_span = other.jsx_import_source_span;
560 }
561 self.directives.extend(other.directives);
562 self.template_tags.extend(other.template_tags);
563 self.template_tags.sort();
564 self.template_tags.dedup();
565 self.unresolved_dynamic.extend(other.unresolved_dynamic);
566 self.ambient_modules.extend(other.ambient_modules);
567 self.augmented_modules.extend(other.augmented_modules);
568 self.parse_diagnostics.extend(other.parse_diagnostics);
569 self.fatal |= other.fatal;
570 }
571}
572
573/// Deletions for the unused specifiers of one export clause (in source order, at least one kept): each run of
574/// adjacent unused specifiers takes the comma after it, or, at the end of the clause, the comma before it.
575fn specifier_runs(specifiers: &[(Span, bool)]) -> Vec<(u32, u32)> {
576 let mut out = Vec::new();
577 let mut i = 0;
578 while i < specifiers.len() {
579 if !specifiers[i].1 {
580 i += 1;
581 continue;
582 }
583 let first = i;
584 while i < specifiers.len() && specifiers[i].1 {
585 i += 1;
586 }
587 let last = i - 1;
588 if let Some(next) = specifiers.get(i) {
589 out.push((specifiers[first].0.start, next.0.start));
590 } else if let Some(before) = first.checked_sub(1).and_then(|k| specifiers.get(k)) {
591 out.push((before.0.end, specifiers[last].0.end));
592 }
593 }
594 out
595}
596
597/// Merges overlapping and touching byte ranges into sorted, disjoint ones.
598#[must_use]
599pub fn coalesce_ranges(ranges: impl IntoIterator<Item = (u32, u32)>) -> Vec<(u32, u32)> {
600 let mut sorted: Vec<(u32, u32)> = ranges.into_iter().filter(|(s, e)| s < e).collect();
601 sorted.sort_unstable();
602 let mut out: Vec<(u32, u32)> = Vec::with_capacity(sorted.len());
603 for (start, end) in sorted {
604 match out.last_mut() {
605 Some(last) if start <= last.1 => last.1 = last.1.max(end),
606 _ => out.push((start, end)),
607 }
608 }
609 out
610}
611
612/// Whether a query (`?raw`, `raw&inline`, with or without its `?`) makes Vite load a file's text instead of the
613/// module, by Vite's own test: a `raw` parameter with no value. `?url` is not one: it names the file to load it some
614/// other way (a worklet, a worker), so the file still counts as used.
615#[must_use]
616pub fn is_source_query(query: &str) -> bool {
617 query
618 .strip_prefix('?')
619 .unwrap_or(query)
620 .split('&')
621 .any(|parameter| parameter == "raw")
622}
623
624/// Options for [`crate::parse_module`].
625#[derive(Debug, Clone, Copy, PartialEq, Eq)]
626pub struct ParseOptions {
627 /// Resolve references to exports inside the file with semantic analysis.
628 pub in_file_references: bool,
629 /// Track members accessed through imported bindings.
630 pub member_accesses: bool,
631}
632
633impl Default for ParseOptions {
634 fn default() -> Self {
635 Self {
636 in_file_references: true,
637 member_accesses: true,
638 }
639 }
640}
641
642const _: fn() = || {
643 fn assert_send_sync<T: Send + Sync>() {}
644 assert_send_sync::<ModuleInfo>();
645 assert_send_sync::<ParseOptions>();
646};