Skip to content

Commit 917d164

Browse files
committed
Reuse single MigrationVisitor + fix config vars
A single MigrationVisitor is now reused instead of constructing a new instance for each dependency. This should also fix the migration of configurable variables when the variable being configured is in an indirect dependency.
1 parent 0b6a061 commit 917d164

File tree

5 files changed

+161
-68
lines changed

5 files changed

+161
-68
lines changed

lib/runner.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import 'dart:io';
88

99
import 'package:args/command_runner.dart';
10+
import 'package:path/path.dart' as p;
1011

1112
import 'src/migrators/module.dart';
1213

@@ -43,7 +44,7 @@ class MigratorRunner extends CommandRunner<Map<Uri, String>> {
4344
if (argResults['dry-run']) {
4445
print('Dry run. Logging migrated files instead of overwriting...\n');
4546
for (var url in migrated.keys) {
46-
print('$url');
47+
print(p.prettyUri(url));
4748
if (argResults['verbose']) {
4849
print('=' * 80);
4950
print(migrated[url]);
@@ -52,9 +53,8 @@ class MigratorRunner extends CommandRunner<Map<Uri, String>> {
5253
}
5354
} else {
5455
for (var url in migrated.keys) {
55-
if (url.scheme != null && url.scheme != "file") {
56-
print("Cannot write to $url");
57-
}
56+
assert(url.scheme == null || url.scheme == "file",
57+
"$url is not a file path.");
5858
if (argResults['verbose']) print("Overwriting $url...");
5959
File(url.toFilePath()).writeAsStringSync(migrated[url]);
6060
}

lib/src/migration_visitor.dart

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,44 +28,48 @@ import 'utils.dart';
2828
/// new instance of itself (using [newInstance]) each time it encounters an
2929
/// `@import` or `@use` rule.
3030
abstract class MigrationVisitor extends RecursiveAstVisitor {
31-
/// The stylesheet being migrated.
32-
final Stylesheet stylesheet;
33-
3431
/// A mapping from URLs to migrated contents for stylesheets already migrated.
35-
final Map<Uri, String> migrated;
32+
final _migrated = <Uri, String>{};
3633

3734
/// True if dependencies should be migrated as well.
3835
final bool migrateDependencies;
3936

4037
/// The patches to be applied to the stylesheet being migrated.
4138
UnmodifiableListView<Patch> get patches => UnmodifiableListView(_patches);
42-
final List<Patch> _patches = [];
43-
44-
/// Constructs a new migration visitor, parsing the stylesheet at [url].
45-
MigrationVisitor(Uri url, this.migrateDependencies,
46-
{Map<Uri, String> migrated})
47-
: stylesheet = parseStylesheet(url),
48-
this.migrated = migrated ?? {};
39+
List<Patch> _patches = [];
4940

50-
/// Returns a new instance of this MigrationVisitor with the same [migrated]
51-
/// and [migrateDependencies] and the new [url].
52-
@protected
53-
MigrationVisitor newInstance(Uri url);
41+
MigrationVisitor({this.migrateDependencies = true});
5442

55-
/// Adds a new patch that should be applied as part of this migration.
56-
@protected
57-
void addPatch(Patch patch) {
58-
_patches.add(patch);
43+
/// Runs a new migration on [url] (and its dependencies, if
44+
/// [migrateDependencies] is true) and returns a map of migrated contents.
45+
Map<Uri, String> run(Uri url) {
46+
visitStylesheet(parseStylesheet(url));
47+
return _migrated;
5948
}
6049

61-
/// Runs the migrator and returns the map of migrated contents.
62-
Map<Uri, String> run() {
63-
visitStylesheet(stylesheet);
50+
/// Visits stylesheet starting with an empty [_patches], adds the migrated
51+
/// contents (if any) to [_migrated], and then restores the previous value of
52+
/// [_patches].
53+
///
54+
/// Migrators with per-file state should override this to store the current
55+
/// file's state before calling the super method and restore it afterwards.
56+
@override
57+
void visitStylesheet(Stylesheet node) {
58+
var oldPatches = _patches;
59+
_patches = [];
60+
super.visitStylesheet(node);
6461
var results = getMigratedContents();
6562
if (results != null) {
66-
migrated[stylesheet.span.file.url] = results;
63+
_migrated[node.span.sourceUrl] = results;
6764
}
68-
return migrated;
65+
_patches = oldPatches;
66+
}
67+
68+
/// Visits the stylesheet at [dependency], resolved relative to [source].
69+
@protected
70+
void visitDependency(Uri dependency, Uri source) {
71+
var stylesheet = parseStylesheet(source.resolveUri(dependency));
72+
visitStylesheet(stylesheet);
6973
}
7074

7175
/// Returns the migrated contents of this file, or null if the file does not
@@ -78,10 +82,11 @@ abstract class MigrationVisitor extends RecursiveAstVisitor {
7882
? Patch.applyAll(patches.first.selection.file, patches)
7983
: null;
8084

81-
/// Resolves [relativeUrl] relative to the current stylesheet's URL.
85+
/// Adds a new patch that should be applied as part of this migration.
8286
@protected
83-
Uri resolveRelativeUrl(Uri relativeUrl) =>
84-
stylesheet.span.file.url.resolveUri(relativeUrl);
87+
void addPatch(Patch patch) {
88+
_patches.add(patch);
89+
}
8590

8691
/// If [migrateDependencies] is enabled, any dynamic imports within
8792
/// this [node] will be migrated before continuing.
@@ -91,7 +96,7 @@ abstract class MigrationVisitor extends RecursiveAstVisitor {
9196
if (migrateDependencies) {
9297
for (var import in node.imports) {
9398
if (import is DynamicImport) {
94-
newInstance(resolveRelativeUrl(Uri.parse(import.url))).run();
99+
visitDependency(Uri.parse(import.url), node.span.sourceUrl);
95100
}
96101
}
97102
}
@@ -103,7 +108,7 @@ abstract class MigrationVisitor extends RecursiveAstVisitor {
103108
visitUseRule(UseRule node) {
104109
super.visitUseRule(node);
105110
if (migrateDependencies) {
106-
newInstance(resolveRelativeUrl(node.url)).run();
111+
visitDependency(node.url, node.span.sourceUrl);
107112
}
108113
}
109114
}

lib/src/migrators/module.dart

Lines changed: 71 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,24 @@ class ModuleMigrator extends Migrator {
2525
final description = "Migrates stylesheets to the new module system.";
2626

2727
/// Global variables defined at any time during the migrator run.
28-
final _variables = normalizedMap<VariableDeclaration>();
28+
final _variables = normalizedMap<List<VariableDeclaration>>();
2929

3030
/// Global mixins defined at any time during the migrator run.
3131
final _mixins = normalizedMap<MixinRule>();
3232

3333
/// Global functions defined at any time during the migrator run.
3434
final _functions = normalizedMap<FunctionRule>();
3535

36+
/// Global variables declared with !default that could be configured.
37+
///final _configurableVariables = normalizedMap<VariableDeclaration>();
38+
3639
/// Runs the module migrator on [entrypoint] and its dependencies and returns
3740
/// a map of migrated contents.
3841
///
3942
/// If [migrateDependencies] is false, the migrator will still be run on
4043
/// dependencies, but they will be excluded from the resulting map.
4144
Map<Uri, String> migrateFile(Uri entrypoint) {
42-
var migrated = _ModuleMigrationVisitor(this, entrypoint).run();
45+
var migrated = _ModuleMigrationVisitor(this).run(entrypoint);
4346
if (!migrateDependencies) {
4447
migrated.removeWhere((url, contents) => url != entrypoint);
4548
}
@@ -50,46 +53,61 @@ class ModuleMigrator extends Migrator {
5053
class _ModuleMigrationVisitor extends MigrationVisitor {
5154
final ModuleMigrator migrator;
5255

53-
/// Constructs a new module migration visitor.
54-
///
55-
/// Note: We always set [migratedDependencies] to true since the module
56-
/// migrator needs to always run on dependencies. The `migrateFile` method of
57-
/// the module migrator will filter out the dependencies' migration results.
58-
_ModuleMigrationVisitor(this.migrator, Uri url, {Map<Uri, String> migrated})
59-
: super(url, true, migrated: migrated);
60-
61-
_ModuleMigrationVisitor newInstance(Uri url) =>
62-
_ModuleMigrationVisitor(migrator, url, migrated: migrated);
63-
6456
/// Namespaces of modules used in this stylesheet.
65-
final _namespaces = <Uri, String>{};
57+
Map<Uri, String> _namespaces = {};
6658

6759
/// Set of additional use rules necessary for referencing members of
6860
/// implicit dependencies / built-in modules.
6961
///
7062
/// This set contains the path provided in the use rule, not the canonical
7163
/// path (e.g. "a" rather than "dir/a.scss").
72-
final _additionalUseRules = Set<String>();
64+
Set<String> _additionalUseRules = Set();
7365

74-
/// Global variables declared with !default that could be configured.
75-
final _configurableVariables = normalizedSet();
66+
/// The URL of the current stylesheet.
67+
Uri _currentUrl;
68+
69+
/// The URL of the last stylesheet that was completely migrated.
70+
Uri _lastUrl;
7671

7772
/// Local variables, mixins, and functions for this migration.
7873
///
7974
/// When at the top level of the stylesheet, this will be null.
8075
LocalScope _localScope;
8176

77+
/// Constructs a new module migration visitor.
78+
///
79+
/// Note: We always set [migratedDependencies] to true since the module
80+
/// migrator needs to always run on dependencies. The `migrateFile` method of
81+
/// the module migrator will filter out the dependencies' migration results.
82+
_ModuleMigrationVisitor(this.migrator) : super(migrateDependencies: true);
83+
8284
/// Returns the migrated contents of this stylesheet, based on [patches] and
8385
/// [_additionalUseRules], or null if the stylesheet does not change.
8486
@override
8587
String getMigratedContents() {
8688
var results = super.getMigratedContents();
8789
if (results == null) return null;
88-
var semicolon = stylesheet.span.file.url.path.endsWith('.sass') ? "" : ";";
90+
var semicolon = _currentUrl.path.endsWith('.sass') ? "" : ";";
8991
var uses = _additionalUseRules.map((use) => '@use "$use"$semicolon\n');
9092
return uses.join("") + results;
9193
}
9294

95+
/// Stores per-file state before visiting [node] and restores it afterwards.
96+
@override
97+
void visitStylesheet(Stylesheet node) {
98+
var oldNamespaces = _namespaces;
99+
var oldAdditionalUseRules = _additionalUseRules;
100+
var oldUrl = _currentUrl;
101+
_namespaces = {};
102+
_additionalUseRules = Set();
103+
_currentUrl = node.span.sourceUrl;
104+
super.visitStylesheet(node);
105+
_namespaces = oldNamespaces;
106+
_additionalUseRules = oldAdditionalUseRules;
107+
_lastUrl = _currentUrl;
108+
_currentUrl = oldUrl;
109+
}
110+
93111
/// Visits the children of [node] with a local scope.
94112
///
95113
/// Note: The children of a stylesheet are at the root, so we should not add
@@ -187,20 +205,34 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
187205
return;
188206
}
189207
// TODO(jathak): Confirm that this import appears before other rules
190-
var url = resolveRelativeUrl(Uri.parse(import.url));
191-
var importMigration = newInstance(url)..run();
192-
_namespaces[importMigration.stylesheet.span.sourceUrl] =
193-
namespaceForPath(import.url);
208+
209+
visitDependency(Uri.parse(import.url), _currentUrl);
210+
_namespaces[_lastUrl] = namespaceForPath(import.url);
194211

195212
var overrides = [];
196-
for (var variable in importMigration._configurableVariables) {
197-
if (migrator._variables.containsKey(variable)) {
198-
var declaration = migrator._variables[variable];
199-
if (namespaceForNode(declaration) == null) {
200-
overrides.add("\$${declaration.name}: ${declaration.expression}");
213+
for (var name in migrator._variables.keys) {
214+
var declarations = migrator._variables[name];
215+
VariableDeclaration lastNonDefault = declarations[0];
216+
for (var i = 1; i < declarations.length; i++) {
217+
var declaration = declarations[i];
218+
if (!declaration.isGuarded) {
219+
lastNonDefault = declaration;
220+
} else if (declaration.span.sourceUrl == _lastUrl) {
221+
if (lastNonDefault.span.sourceUrl == _currentUrl) {
222+
// This configurable variable was set within the current stylesheet,
223+
// so add to overrides.
224+
overrides.add("\$$name: ${lastNonDefault.expression}");
225+
} else if (lastNonDefault.span.sourceUrl != _lastUrl) {
226+
// A downstream stylesheet configures this variable, so forward it.
227+
var semicolon = _currentUrl.path.endsWith('.sass') ? '' : ';';
228+
addPatch(patchBefore(
229+
node, '@forward ${import.span.text} show \$$name$semicolon\n'));
230+
// Add a fake variable declaration so that downstream stylesheets
231+
// configure this module instead of the one we forwarded.
232+
declarations.insert(i + 1,
233+
VariableDeclaration(name, null, node.span, guarded: true));
234+
}
201235
}
202-
// TODO(jathak): Remove this declaration from the current stylesheet if
203-
// it's not referenced before this point.
204236
}
205237
}
206238
var config = "";
@@ -245,7 +277,9 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
245277
return;
246278
}
247279
if (!migrator._variables.containsKey(node.name)) return;
248-
var namespace = namespaceForNode(migrator._variables[node.name]);
280+
var lastRealDeclaration = migrator._variables[node.name]
281+
.lastWhere((node) => node.expression != null);
282+
var namespace = namespaceForNode(lastRealDeclaration);
249283
if (namespace == null) return;
250284
addPatch(Patch(node.span, "\$$namespace.${node.name}"));
251285
}
@@ -261,14 +295,16 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
261295
/// it exists, or as a global variable otherwise.
262296
void declareVariable(VariableDeclaration node) {
263297
if (_localScope == null || node.isGlobal) {
264-
if (node.isGuarded) {
265-
_configurableVariables.add(node.name);
298+
/*if (node.isGuarded) {
299+
migrator._configurableVariables[node.name] = node;
266300
267301
// Don't override if variable already exists.
268302
migrator._variables.putIfAbsent(node.name, () => node);
269303
} else {
270304
migrator._variables[node.name] = node;
271-
}
305+
}*/
306+
migrator._variables.putIfAbsent(node.name, () => []);
307+
migrator._variables[node.name].add(node);
272308
} else {
273309
_localScope.variables.add(node.name);
274310
}
@@ -297,11 +333,11 @@ class _ModuleMigrationVisitor extends MigrationVisitor {
297333
/// Finds the namespace for the stylesheet containing [node], adding a new use
298334
/// rule if necessary.
299335
String namespaceForNode(SassNode node) {
300-
if (node.span.sourceUrl == stylesheet.span.sourceUrl) return null;
336+
if (node.span.sourceUrl == _currentUrl) return null;
301337
if (!_namespaces.containsKey(node.span.sourceUrl)) {
302338
/// Add new use rule for indirect dependency
303339
var relativePath = p.relative(node.span.sourceUrl.path,
304-
from: p.dirname(stylesheet.span.sourceUrl.path));
340+
from: p.dirname(_currentUrl.path));
305341
var basename = p.basenameWithoutExtension(relativePath);
306342
if (basename.startsWith('_')) basename = basename.substring(1);
307343
var simplePath = p.relative(p.join(p.dirname(relativePath), basename));
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<==> arguments
2+
--migrate-deps
3+
4+
<==> input/entrypoint.scss
5+
$config: red;
6+
@import "direct";
7+
8+
<==> input/_direct.scss
9+
@import "indirect";
10+
11+
<==> input/_indirect.scss
12+
$config: green !default;
13+
14+
<==> output/entrypoint.scss
15+
$config: red;
16+
@use "direct" with (
17+
$config: red
18+
);
19+
20+
<==> output/_direct.scss
21+
@forward "indirect" show $config;
22+
@use "indirect";
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<==> arguments
2+
--migrate-deps
3+
4+
<==> input/entrypoint.scss
5+
$config: red;
6+
@import "direct";
7+
8+
<==> input/_direct.scss
9+
@import "indirect";
10+
11+
a {
12+
b: $config;
13+
}
14+
15+
<==> input/_indirect.scss
16+
$config: green !default;
17+
18+
<==> output/entrypoint.scss
19+
$config: red;
20+
@use "direct" with (
21+
$config: red
22+
);
23+
24+
<==> output/_direct.scss
25+
@forward "indirect" show $config;
26+
@use "indirect";
27+
28+
a {
29+
b: $indirect.config;
30+
}

0 commit comments

Comments
 (0)