Skip to content

Commit 6984c16

Browse files
authored
Better handling of late imports (#280)
* Better handling of late imports Fixes #278. Also fixes a bug where a file that uses and imports the same dependency would be migrated to duplicate `@use` rules. * Code review
1 parent 74bfa89 commit 6984c16

20 files changed

+443
-35
lines changed

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
1+
## 2.4.0
2+
3+
### Module System Migration
4+
5+
* Better handling of late `@import` rules. Previously, these were treated
6+
identically to nested imports, but now they can be hoisted to the top of the
7+
file where `@use` is allowed if they do not emit any CSS.
8+
9+
To allow the migrator to hoist even late imports that _do_ emit CSS, use the
10+
`--unsafe-hoist` flag. `@import` rules that emit CSS will still be converted
11+
to `meta.load-css()` even with this flag if no Sass members are referenced
12+
from them.
13+
14+
If there are any plain CSS at-rules that are used by your post-processing
15+
tools but never actually result in meaningful CSS, you can pass them
16+
(without the `@`) to `--safe-at-rule` so that the migrator doesn't consider
17+
them to emit CSS when handling late imports.
18+
19+
The migrator's behavior when encountering actual nested imports remains
20+
unchanged.
21+
22+
* Fix a bug that resulted in duplicate `@use` rules when migrating stylesheets
23+
that contained both a `@use` and an `@import` of the same dependency.
24+
125
## 2.3.3
226

327
### Module System Migration

lib/src/migrators/module.dart

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ class ModuleMigrator extends Migrator {
3535
final argParser = ArgParser()
3636
..addFlag('built-in-only',
3737
help: 'Migrates global functions without migrating @import.')
38+
..addMultiOption('safe-at-rule',
39+
help: 'CSS at-rules (without the @) used by postprocessing tools '
40+
'that should not be considered to be emitting CSS.')
41+
..addFlag('unsafe-hoist',
42+
help: 'Allow the migrator to hoist late imports to the top of the '
43+
'file even when they emit CSS.')
3844
..addMultiOption('remove-prefix',
3945
abbr: 'p',
4046
help: 'Removes PREFIX from all migrated member names.\n'
@@ -94,14 +100,16 @@ class ModuleMigrator extends Migrator {
94100
'which prefixed members to forward.');
95101
}
96102

97-
var references = References(importCache, stylesheet, importer);
103+
var references = References(importCache, stylesheet, importer,
104+
safeAtRules: argResults!['safe-at-rule'] as List<String>);
98105
var visitor = _ModuleMigrationVisitor(
99106
importCache, references, globalResults!['load-path'] as List<String>,
100107
migrateDependencies: migrateDependencies,
101108
builtInOnly: builtInOnly,
102109
prefixesToRemove: (argResults!['remove-prefix'] as List<String>)
103110
.map((prefix) => prefix.replaceAll('_', '-')),
104-
forwards: forwards);
111+
forwards: forwards,
112+
unsafeHoist: argResults!['unsafe-hoist'] as bool);
105113
var migrated = visitor.run(stylesheet, importer);
106114
_filesWithRenamedDeclarations.addAll(
107115
{for (var member in visitor.renamedMembers.keys) member.sourceUrl});
@@ -156,6 +164,11 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
156164
assertInStylesheet(__builtInUseRules, '_builtInUseRules');
157165
Set<String>? __builtInUseRules;
158166

167+
/// Set of `@use` rules hoisted from `@import` rules later in the stylesheet.
168+
Set<String> get _hoistedUseRules =>
169+
assertInStylesheet(__hoistedUseRules, '_hoistedUseRules');
170+
Set<String>? __hoistedUseRules;
171+
159172
/// Set of additional `@use` rules for stylesheets at a load path.
160173
Set<String> get _additionalLoadPathUseRules => assertInStylesheet(
161174
__additionalLoadPathUseRules, '_additionalLoadPathUseRules');
@@ -184,6 +197,9 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
184197
/// from any member visible at the entrypoint.
185198
var _needsImportOnly = false;
186199

200+
/// The stylesheet currently being migrated.
201+
late Stylesheet _currentStylesheet;
202+
187203
/// Set of variables declared outside the current stylesheet that overrode
188204
/// `!default` variables within the current stylesheet.
189205
Set<MemberDeclaration<VariableDeclaration>> get _configuredVariables =>
@@ -211,6 +227,10 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
211227
/// Whether to migrate only global functions, leaving `@import` rules as-is.
212228
final bool builtInOnly;
213229

230+
/// Whether to allow hoisting imports to the top of the file even when they
231+
/// emit CSS.
232+
final bool unsafeHoist;
233+
214234
/// Constructs a new module migration visitor.
215235
///
216236
/// [importCache] must be the same one used by [references].
@@ -226,6 +246,7 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
226246
super.importCache, this.references, List<String> loadPaths,
227247
{required super.migrateDependencies,
228248
required this.builtInOnly,
249+
required this.unsafeHoist,
229250
Iterable<String> prefixesToRemove = const [],
230251
this.forwards = const {}})
231252
: loadPaths = List.unmodifiable(
@@ -421,10 +442,12 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
421442
/// visiting it and restores the per-file state afterwards.
422443
@override
423444
void visitStylesheet(Stylesheet node) {
445+
_currentStylesheet = node;
424446
var oldNamespaces = __namespaces;
425447
var oldForwardedUrls = __forwardedUrls;
426448
var oldUsedUrls = __usedUrls;
427449
var oldBuiltInUseRules = __builtInUseRules;
450+
var oldHoistedUseRules = __hoistedUseRules;
428451
var oldLoadPathUseRules = __additionalLoadPathUseRules;
429452
var oldRelativeUseRules = __additionalRelativeUseRules;
430453
var oldBeforeFirstImport = _beforeFirstImport;
@@ -438,10 +461,11 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
438461
?.$2 ??
439462
rule.url: rule.namespace
440463
};
464+
__usedUrls = _namespaces.keys.toSet();
441465
_determineNamespaces(node.span.sourceUrl!, _namespaces);
442-
__usedUrls = {};
443466
__forwardedUrls = {};
444467
__builtInUseRules = {};
468+
__hoistedUseRules = {};
445469
__additionalLoadPathUseRules = {};
446470
__additionalRelativeUseRules = {};
447471
_beforeFirstImport = null;
@@ -452,6 +476,7 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
452476
__forwardedUrls = oldForwardedUrls;
453477
__usedUrls = oldUsedUrls;
454478
__builtInUseRules = oldBuiltInUseRules;
479+
__hoistedUseRules = oldHoistedUseRules;
455480
__additionalLoadPathUseRules = oldLoadPathUseRules;
456481
__additionalRelativeUseRules = oldRelativeUseRules;
457482
_beforeFirstImport = oldBeforeFirstImport;
@@ -474,7 +499,8 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
474499
useRulesToString(_builtInUseRules)),
475500
beforeExisting: true);
476501
}
477-
var extras = useRulesToString(_additionalLoadPathUseRules) +
502+
var extras = useRulesToString(_hoistedUseRules) +
503+
useRulesToString(_additionalLoadPathUseRules) +
478504
useRulesToString(_additionalRelativeUseRules) +
479505
_getAdditionalForwardRules();
480506
if (extras == '') return;
@@ -795,7 +821,8 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
795821
}
796822
String rulesText;
797823

798-
var migratedRules = <String>[];
824+
var inPlaceUseRules = <String>[];
825+
var loadCssRules = <String>[];
799826

800827
var indent = ' ' * node.span.start.column;
801828

@@ -816,23 +843,35 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
816843
}
817844
}
818845
if (ruleUrl != null) {
846+
var canonicalUrl = importCache
847+
.canonicalize(ruleUrl, baseImporter: importer, baseUrl: currentUrl)!
848+
.$2;
849+
var isNested = !_currentStylesheet.children.contains(node);
819850
if (builtInOnly) {
820851
if (migrateDependencies) {
821852
_upstreamStylesheets.add(currentUrl);
822853
visitDependency(ruleUrl, import.span);
823854
_upstreamStylesheets.remove(currentUrl);
824855
}
825856
} else if (_useAllowed) {
826-
migratedRules.addAll(_migrateImportToRules(ruleUrl, import.span));
857+
inPlaceUseRules.addAll(_migrateImportToRules(ruleUrl, import.span));
858+
} else if (!isNested &&
859+
(!(references.fileEmitsCss[canonicalUrl] ?? true) ||
860+
(unsafeHoist &&
861+
references.anyMemberReferenced(
862+
canonicalUrl, currentUrl)))) {
863+
_hoistedUseRules.addAll(_migrateImportToRules(ruleUrl, import.span));
827864
} else {
828-
migratedRules.add(_migrateImportToLoadCss(ruleUrl, import.span)
829-
.replaceAll('\n', '\n$indent'));
865+
loadCssRules.add(
866+
_migrateImportToLoadCss(ruleUrl, import.span, isNested)
867+
.replaceAll('\n', '\n$indent'));
830868
}
831869
}
832870
}
833871
if (builtInOnly) return;
834872

835-
rulesText = migratedRules.join('$semicolon\n$indent');
873+
rulesText =
874+
[...inPlaceUseRules, ...loadCssRules].join('$semicolon\n$indent');
836875
if (rulesText.isEmpty) {
837876
var span = node.span.extendIfMatches(RegExp(' *$semicolon\n?'));
838877
addPatch(patchDelete(span));
@@ -894,8 +933,9 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
894933
if ((rules.isEmpty && normalForwardRules == null) ||
895934
withClause.isNotEmpty ||
896935
references.anyMemberReferenced(canonicalUrl, currentUrl)) {
897-
_usedUrls.add(canonicalUrl);
898-
rules.add('@use $quotedUrl$asClause$withClause');
936+
if (_usedUrls.add(canonicalUrl)) {
937+
rules.add('@use $quotedUrl$asClause$withClause');
938+
}
899939
}
900940
if (normalForwardRules != null) rules.addAll(normalForwardRules);
901941
return rules;
@@ -906,7 +946,7 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
906946
///
907947
/// This is used for migrating `@import` rules that are nested or appear after
908948
/// some other rules.
909-
String _migrateImportToLoadCss(Uri ruleUrl, FileSpan context) {
949+
String _migrateImportToLoadCss(Uri ruleUrl, FileSpan context, bool isNested) {
910950
var oldUnreferencable = _unreferencable;
911951
_unreferencable = UnreferencableMembers(_unreferencable);
912952
for (var declaration in references.allDeclarations) {
@@ -927,7 +967,11 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
927967
_unreferencable = oldUnreferencable;
928968
for (var declaration in references.allDeclarations) {
929969
if (declaration.sourceUrl != canonicalUrl) continue;
930-
_unreferencable.add(declaration, UnreferencableType.fromNestedImport);
970+
_unreferencable.add(
971+
declaration,
972+
isNested
973+
? UnreferencableType.fromNestedImport
974+
: UnreferencableType.fromLateImport);
931975
}
932976

933977
var meta = _findOrAddBuiltInNamespace('meta');

lib/src/migrators/module/references.dart

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ class References {
8585
/// regular file is forwarded by the import-only file).
8686
final Map<Uri, ForwardRule?> orphanImportOnlyFiles;
8787

88+
/// A map from stylesheet URLs to whether or not they possibly emit CSS,
89+
///
90+
/// A file is considered to emit CSS if it contains style rules, contains
91+
/// any top-level `@include` rules or plain CSS at-rules, or depends on any
92+
/// file that itself emits CSS.
93+
final Map<Uri, bool> fileEmitsCss;
94+
8895
/// An iterable of all member declarations.
8996
Iterable<MemberDeclaration> get allDeclarations =>
9097
variables.values.followedBy(mixins.values).followedBy(functions.values);
@@ -142,7 +149,8 @@ class References {
142149
Set<MemberDeclaration> globalDeclarations,
143150
Map<MemberDeclaration, Set<Uri>> libraries,
144151
Map<SassReference, ReferenceSource> sources,
145-
Map<Uri, ForwardRule?> orphanImportOnlyFiles)
152+
Map<Uri, ForwardRule?> orphanImportOnlyFiles,
153+
Map<Uri, bool> fileEmitsCss)
146154
: variables = UnmodifiableBidirectionalMapView(variables),
147155
variableReassignments =
148156
UnmodifiableBidirectionalMapView(variableReassignments),
@@ -158,13 +166,16 @@ class References {
158166
entry.key: UnmodifiableSetView(entry.value)
159167
}),
160168
sources = UnmodifiableMapView(sources),
161-
orphanImportOnlyFiles = UnmodifiableMapView(orphanImportOnlyFiles);
169+
orphanImportOnlyFiles = UnmodifiableMapView(orphanImportOnlyFiles),
170+
fileEmitsCss = UnmodifiableMapView(fileEmitsCss);
162171

163172
/// Constructs a new [References] object based on a [stylesheet] (imported by
164173
/// [importer]) and its dependencies.
165174
factory References(
166-
ImportCache importCache, Stylesheet stylesheet, Importer importer) =>
167-
_ReferenceVisitor(importCache).build(stylesheet, importer);
175+
ImportCache importCache, Stylesheet stylesheet, Importer importer,
176+
{Iterable<String> safeAtRules = const []}) =>
177+
_ReferenceVisitor(importCache, safeAtRules.toSet())
178+
.build(stylesheet, importer);
168179
}
169180

170181
/// A visitor that builds a References object.
@@ -183,6 +194,7 @@ class _ReferenceVisitor extends ScopedAstVisitor {
183194
final _libraries = <MemberDeclaration, Set<Uri>>{};
184195
final _sources = <SassReference, ReferenceSource>{};
185196
final _orphanImportOnlyFiles = <Uri, ForwardRule?>{};
197+
final _fileEmitsCss = <Uri, bool>{};
186198

187199
/// Mapping from canonical stylesheet URLs to the global scope of the module
188200
/// contained within it.
@@ -229,6 +241,10 @@ class _ReferenceVisitor extends ScopedAstVisitor {
229241
/// stylesheet.
230242
late Importer _importer;
231243

244+
/// Whether any dependency of the current file emits CSS (which then means
245+
/// that this file emits CSS even if it doesn't directly).
246+
late bool _dependencyEmitsCss;
247+
232248
/// If the current stylesheet is an import-only file, this starts as true and
233249
/// is changed to false if it forwards its regular counterpart.
234250
///
@@ -241,7 +257,10 @@ class _ReferenceVisitor extends ScopedAstVisitor {
241257
/// The last `@forward` rule to be visited that was not an import-only file.
242258
ForwardRule? _lastRegularForward;
243259

244-
_ReferenceVisitor(this.importCache);
260+
/// CSS at-rules that should be considered to emit CSS.
261+
final Set<String> safeAtRules;
262+
263+
_ReferenceVisitor(this.importCache, this.safeAtRules);
245264

246265
/// Constructs a new References object based on a [stylesheet] (imported by
247266
/// [importer]) and its dependencies.
@@ -253,6 +272,7 @@ class _ReferenceVisitor extends ScopedAstVisitor {
253272
_moduleScopes[_currentUrl] = currentScope;
254273
_declarationSources = {};
255274
_moduleSources[_currentUrl] = _declarationSources;
275+
_dependencyEmitsCss = false;
256276
visitStylesheet(stylesheet);
257277

258278
for (var variable in currentScope.variables.values) {
@@ -274,7 +294,8 @@ class _ReferenceVisitor extends ScopedAstVisitor {
274294
_globalDeclarations,
275295
_libraries,
276296
_sources,
277-
_orphanImportOnlyFiles);
297+
_orphanImportOnlyFiles,
298+
_fileEmitsCss);
278299
}
279300

280301
/// Checks any remaining [_unresolvedReferences] to see if they match a
@@ -318,6 +339,8 @@ class _ReferenceVisitor extends ScopedAstVisitor {
318339
var oldNamespaces = _namespaces;
319340
var oldUrl = _currentUrl;
320341
var oldOrphaned = _isOrphanImportOnly;
342+
var oldDependencyEmitsCss = _dependencyEmitsCss;
343+
_dependencyEmitsCss = false;
321344
_namespaces = {};
322345
_currentUrl = node.span.sourceUrl!;
323346
_isOrphanImportOnly = isImportOnlyFile(_currentUrl);
@@ -328,11 +351,30 @@ class _ReferenceVisitor extends ScopedAstVisitor {
328351
? _lastRegularForward
329352
: null;
330353
}
354+
var emitsCss = _dependencyEmitsCss || node.children.any(_statementEmitsCss);
355+
_fileEmitsCss[_currentUrl] = emitsCss;
331356
_isOrphanImportOnly = oldOrphaned;
332357
_namespaces = oldNamespaces;
333358
_currentUrl = oldUrl;
359+
_dependencyEmitsCss = oldDependencyEmitsCss || emitsCss;
334360
}
335361

362+
/// Returns true if [statement] emits any CSS.
363+
bool _statementEmitsCss(Statement statement) => switch (statement) {
364+
StyleRule() || IncludeRule() || MediaRule() || SupportsRule() => true,
365+
AtRule(:var name) => !safeAtRules.contains(name.asPlain),
366+
EachRule(:var children) ||
367+
ForRule(:var children) ||
368+
MediaRule(:var children) ||
369+
WhileRule(:var children) =>
370+
children.any(_statementEmitsCss),
371+
IfRule(:var clauses) =>
372+
clauses.any((clause) => clause.children.any(_statementEmitsCss)),
373+
// All other statement types either don't emit CSS or are always
374+
// nested inside of a CSS-emitting statement.
375+
_ => false,
376+
};
377+
336378
/// Visits the stylesheet this `@import` rule points to using the existing
337379
/// global scope.
338380
@override

0 commit comments

Comments
 (0)