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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
//! Issue #2374: `default` is one importable name that both sides may spell two
//! ways. An export declares it as `export default x` or as
//! `export { x as default }`; an import names it as `import x from './impl'`
//! or as `import { default as x } from './impl'`, and an ambient
//! `declare module '<specifier>' { export { default } from './impl' }` records
//! one named type-space import per specifier. Every pairing must credit the
//! target's default export.
//!
//! Two shapes must stay uncredited. A plain `export * from './impl'`: an ES
//! star re-export never forwards `default`, however the target spells it. And
//! a CSS Module class spelled `.default`, in any of the four stylesheet
//! syntaxes the extractor treats as a CSS Module: a stylesheet exports class
//! names rather than a module default, so only the member accesses the
//! consumer writes credit its classes.
use super::common::{create_config, fixture_path};
const FIXTURE: &str = "issue-2374-default-specifier";
/// The boundary fixtures stay separate from `FIXTURE`: each declares a further
/// `ExportName::Named("default")`, which the pre-existing `duplicate_exports`
/// grouping would treat as an ordinary colliding name and report against
/// `FIXTURE`'s `export { inner as default }`.
const CJS_FIXTURE: &str = "issue-2374-cjs-default-property";
const CSS_FIXTURE: &str = "issue-2374-css-module-default-class";
const STAR_NAMED_DEFAULT_FIXTURE: &str = "issue-2374-star-named-default";
fn unused_export_pairs(results: &fallow_core::results::AnalysisResults) -> Vec<(String, String)> {
results
.unused_exports
.iter()
.map(|e| {
(
e.export.path.to_string_lossy().replace('\\', "/"),
e.export.export_name.clone(),
)
})
.collect()
}
fn unused_defaults(results: &fallow_core::results::AnalysisResults) -> Vec<String> {
unused_export_pairs(results)
.into_iter()
.filter(|(_, name)| name == "default")
.map(|(path, _)| path)
.collect()
}
#[test]
fn default_specifiers_credit_the_target_default_export() {
let results = fallow_core::analyze(&create_config(fixture_path(FIXTURE)))
.expect("analysis should succeed");
let unused_defaults = unused_defaults(&results);
let pairs = unused_export_pairs(&results);
// `import { default as Aliased } from './alias-impl'` binds the same
// export as `import Aliased from './alias-impl'`.
// `import LocalDefault from './local-default'` binds the same export as
// the `export { inner as default }` that declares it.
// The `export { default } from` chain carries the binding through two hops
// to `chain-deep.ts`, and the top of that chain is itself consumed with a
// `default` specifier, so every hop keeps its credit.
for path in [
"src/alias-impl.ts",
"src/local-default.ts",
"src/chain-top.ts",
"src/chain-mid.ts",
"src/chain-deep.ts",
] {
assert!(
!unused_defaults.iter().any(|p| p.ends_with(path)),
"{path}: a `default` specifier names the default export and must credit it; unused \
defaults: {unused_defaults:?}"
);
}
// A `default` specifier in a mixed statement leaves its named siblings
// alone: `named` is used, `aliasSibling` is not.
assert!(
!pairs
.iter()
.any(|(path, name)| path.ends_with("src/alias-impl.ts") && name == "named"),
"a named sibling of a `default` specifier keeps its credit: {pairs:?}"
);
assert!(
pairs
.iter()
.any(|(path, name)| path.ends_with("src/alias-impl.ts") && name == "aliasSibling"),
"an export no import names must keep reporting: {pairs:?}"
);
}
#[test]
fn ambient_default_re_exports_credit_the_target_default_export() {
let results = fallow_core::analyze(&create_config(fixture_path(FIXTURE)))
.expect("analysis should succeed");
let unused_defaults = unused_defaults(&results);
let pairs = unused_export_pairs(&results);
// `declare module 'untyped-default' { export { default } from './ambient-impl' }`
// and the mixed `export { default as Impl, Y as Z } from './ambient-mixed'`
// state that the target's default export is reachable through the declared
// module id.
for path in ["src/ambient-impl.ts", "src/ambient-mixed.ts"] {
assert!(
!unused_defaults.iter().any(|p| p.ends_with(path)),
"{path}: an ambient `default` re-export must credit the target's default export; \
unused defaults: {unused_defaults:?}"
);
}
// The named half of the mixed statement was already credited and stays so,
// while the sibling no specifier names keeps reporting. Both pin that the
// fix credits the default without widening the ambient surface.
assert!(
!pairs
.iter()
.any(|(path, name)| path.ends_with("src/ambient-mixed.ts") && name == "Y"),
"the named half of an ambient re-export keeps its credit: {pairs:?}"
);
for (file, name) in [
("src/ambient-impl.ts", "ambientSibling"),
("src/ambient-mixed.ts", "ambientMixedSibling"),
] {
assert!(
pairs
.iter()
.any(|(path, export)| path.ends_with(file) && export == name),
"{file}: `{name}` is named by no ambient specifier and must keep reporting: {pairs:?}"
);
}
}
/// Deliberate negative control: a plain `export *` forwards every named export
/// and never `default`, so crediting a `default` specifier must not leak
/// through one.
#[test]
fn plain_star_re_export_still_does_not_forward_default() {
let results = fallow_core::analyze(&create_config(fixture_path(FIXTURE)))
.expect("analysis should succeed");
let unused_defaults = unused_defaults(&results);
let pairs = unused_export_pairs(&results);
assert!(
unused_defaults
.iter()
.any(|p| p.ends_with("src/star-impl.ts")),
"a plain `export *` must not forward the target's default export; unused defaults: \
{unused_defaults:?}"
);
// The named export the star does forward stays credited, so the control
// proves the star edge is live rather than absent.
assert!(
!pairs
.iter()
.any(|(path, name)| path.ends_with("src/star-impl.ts") && name == "starNamed"),
"the star re-export forwards named exports: {pairs:?}"
);
}
/// Deliberate negative control: a plain `export *` forwards named exports and
/// never `default`, including when the target writes its default as
/// `export { hidden as default }`. That spelling is the one the fix relocates
/// into the default slot, so it is the spelling the star boundary has to be
/// pinned on.
#[test]
fn plain_star_re_export_does_not_forward_a_named_default_either() {
let results = fallow_core::analyze(&create_config(fixture_path(STAR_NAMED_DEFAULT_FIXTURE)))
.expect("analysis should succeed");
let unused_defaults = unused_defaults(&results);
let pairs = unused_export_pairs(&results);
assert!(
unused_defaults
.iter()
.any(|p| p.ends_with("src/star-impl.ts")),
"`export {{ hidden as default }}` is not forwarded by a plain `export *`; unused \
defaults: {unused_defaults:?}"
);
// The named export the star does forward stays credited, so the control
// proves the star edge is live rather than absent.
assert!(
!pairs
.iter()
.any(|(path, name)| path.ends_with("src/star-impl.ts") && name == "starNamed"),
"the star re-export forwards named exports: {pairs:?}"
);
}
/// Deliberate negative control: a CSS Module exports class names, so a class
/// spelled `.default` is an ordinary class. A plain
/// `import classes from './classes.module.css'` binds the whole class map and
/// names no single class, so only the members the consumer writes are
/// credited and `.default` must keep reporting.
///
/// The exception covers every stylesheet syntax the extractor treats as a CSS
/// Module, so `.module.less` and `.module.sass` siblings carry the same pin
/// (issue #2374 review round 2). Every syntax uses the same member-narrowing
/// rule, so the accessed non-`default` class stays credited too.
#[test]
fn a_css_module_class_named_default_is_not_a_default_export() {
let results = fallow_core::analyze(&create_config(fixture_path(CSS_FIXTURE)))
.expect("analysis should succeed");
let pairs = unused_export_pairs(&results);
for (stylesheet, name) in [
("src/classes.module.css", "default"),
("src/classes.module.css", "unusedClass"),
("src/classes.module.less", "default"),
("src/classes.module.sass", "default"),
] {
assert!(
pairs
.iter()
.any(|(path, export)| path.ends_with(stylesheet) && export == name),
"{stylesheet}: `{name}` is credited by no member access and must keep reporting: \
{pairs:?}"
);
}
// The class the consumer does write stays credited, so the control proves
// the stylesheet edge is live rather than absent.
for (stylesheet, name) in [
("src/classes.module.css", "usedClass"),
("src/classes.module.less", "lessClass"),
("src/classes.module.sass", "sassClass"),
] {
assert!(
!pairs
.iter()
.any(|(path, export)| path.ends_with(stylesheet) && export == name),
"{stylesheet}: a class the consumer accesses keeps its credit: {pairs:?}"
);
}
// Every stylesheet is imported, so none of them may fall out as an unused
// file: that would make the `default` assertions above pass vacuously.
let unused_files: Vec<String> = results
.unused_files
.iter()
.map(|f| f.file.path.to_string_lossy().replace('\\', "/"))
.collect();
for stylesheet in [
"src/classes.module.css",
"src/classes.module.less",
"src/classes.module.sass",
] {
assert!(
!unused_files.iter().any(|p| p.ends_with(stylesheet)),
"{stylesheet} is imported and must resolve: {unused_files:?}"
);
}
}
/// A CommonJS `exports.default = x` declares the same binding
/// `import x from './m'` reads, and the extractor records it as
/// `Named("default")` exactly like `export { x as default }`. Crediting it is
/// deliberate: the graph cannot tell the two apart, and leaving it uncredited
/// reported the default of every transpiled CommonJS module as unused.
#[test]
fn a_commonjs_default_property_export_answers_a_plain_default_import() {
let results = fallow_core::analyze(&create_config(fixture_path(CJS_FIXTURE)))
.expect("analysis should succeed");
let unused_defaults = unused_defaults(&results);
let pairs = unused_export_pairs(&results);
assert!(
!unused_defaults
.iter()
.any(|p| p.ends_with("src/cjs-default.cjs")),
"`exports.default` is the binding a plain default import reads; unused defaults: \
{unused_defaults:?}"
);
assert!(
pairs
.iter()
.any(|(path, name)| path.ends_with("src/cjs-default.cjs") && name == "cjsSibling"),
"a sibling property export no import names must keep reporting: {pairs:?}"
);
}
/// A named re-export chain forwards exactly the specifiers written on it, so
/// crediting the default through two hops must not launder the sibling.
#[test]
fn default_re_export_chain_does_not_launder_named_siblings() {
let results = fallow_core::analyze(&create_config(fixture_path(FIXTURE)))
.expect("analysis should succeed");
let pairs = unused_export_pairs(&results);
assert!(
pairs
.iter()
.any(|(path, name)| path.ends_with("src/chain-deep.ts") && name == "chainSibling"),
"`chainSibling` is on no `export {{ default }}` specifier and must keep reporting: \
{pairs:?}"
);
}