Skip to main content

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};