-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathmigration_visitor.dart
184 lines (162 loc) · 6.25 KB
/
migration_visitor.dart
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
// Copyright 2019 Google LLC
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:collection';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart';
// The sass package's API is not necessarily stable. It is being imported with
// the Sass team's explicit knowledge and approval. See
// https://github.com/sass/dart-sass/issues/236.
import 'package:sass/src/ast/sass.dart';
import 'package:sass/src/importer.dart';
import 'package:sass/src/import_cache.dart';
import 'package:sass/src/visitor/recursive_ast.dart';
import 'exception.dart';
import 'patch.dart';
import 'utils.dart';
/// A visitor that migrates a stylesheet.
///
/// When [run] is called, this visitor traverses a stylesheet's AST, allowing
/// subclasses to override one or more methods and add to [patches]. Once the
/// stylesheet has been visited, the migrated contents (based on [patches]) will
/// be stored in [migrator]'s [migrated] map.
///
/// If [migrateDependencies] is enabled, this visitor will construct and run a
/// new instance of itself (using [newInstance]) each time it encounters an
/// `@import` or `@use` rule.
abstract class MigrationVisitor extends RecursiveAstVisitor {
/// A mapping from URLs to migrated contents for stylesheets already migrated.
final _migrated = <Uri, String>{};
/// True if dependencies should be migrated as well.
@protected
final bool migrateDependencies;
/// Cache used to load stylesheets.
@protected
final ImportCache importCache;
/// Map of missing dependency URLs to the spans that import/use them.
Map<Uri, FileSpan> get missingDependencies =>
UnmodifiableMapView(_missingDependencies);
final _missingDependencies = <Uri, FileSpan>{};
/// The patches to be applied to the stylesheet being migrated.
@protected
List<Patch> get patches => UnmodifiableListView(assertNotNull(_patches));
List<Patch>? _patches;
/// URL of the stylesheet currently being migrated.
@protected
Uri get currentUrl => assertNotNull(_currentUrl);
Uri? _currentUrl;
/// The importer that's being used to resolve relative imports.
///
/// If this is `null`, relative imports aren't supported in the current
/// stylesheet.
@protected
Importer get importer => _importer;
late Importer _importer;
MigrationVisitor(this.importCache, this.migrateDependencies);
/// Runs a new migration on [stylesheet] (and its dependencies, if
/// [migrateDependencies] is true) and returns a map of migrated contents.
Map<Uri, String> run(Stylesheet stylesheet, Importer importer) {
_importer = importer;
visitStylesheet(stylesheet);
return _migrated;
}
/// Visits stylesheet starting with an empty [_patches], adds the migrated
/// contents (if any) to [_migrated], and then restores the previous value of
/// [_patches].
///
/// Migrators with per-file state should override this to store the current
/// file's state before calling the super method and restore it afterwards.
@override
void visitStylesheet(Stylesheet node) {
var oldPatches = _patches;
var oldUrl = _currentUrl;
_patches = [];
_currentUrl = node.span.sourceUrl!;
super.visitStylesheet(node);
beforePatch(node);
var results = patches.isNotEmpty
? Patch.applyAll(patches.first.selection.file, patches)
: null;
if (results != null) {
var existingResults = _migrated[_currentUrl];
if (existingResults != null && existingResults != results) {
throw MigrationException(
"The migrator has found multiple possible migrations for "
"${p.prettyUri(_currentUrl)}, depending on the context in which "
"it's loaded.");
}
_migrated[currentUrl] = results;
}
_patches = oldPatches;
_currentUrl = oldUrl;
}
/// Called after visiting [node], but before patches are applied.
///
/// A migrator should override this if it needs to add any additional patches
/// after a stylesheet is visited.
@protected
void beforePatch(Stylesheet node) {}
/// Visits the stylesheet at [dependency], resolved based on the current
/// stylesheet's URL and importer.
@protected
void visitDependency(Uri dependency, FileSpan context,
{bool forImport = false}) {
var result = importCache.import(dependency,
baseImporter: _importer, baseUrl: _currentUrl, forImport: forImport);
if (result != null) {
// If [dependency] comes from a non-relative import, don't migrate it,
// because it's likely to be outside the user's repository and may even be
// authored by a different person.
//
// TODO(nweiz): Add a flag to override this behavior for load paths
// (#104).
if (result.item1 != _importer) return;
var oldImporter = _importer;
_importer = result.item1;
var stylesheet = result.item2;
visitStylesheet(stylesheet);
_importer = oldImporter;
} else {
_missingDependencies.putIfAbsent(
context.sourceUrl!.resolveUri(dependency), () => context);
}
}
/// Adds a new patch that should be applied to the current stylesheet.
///
/// If [beforeExisting] is true, this patch will be added to the beginning of
/// the patch list. This should be used for insertion patches that should be
/// inserted before any existing insertion patches at the same location.
@protected
void addPatch(Patch patch, {bool beforeExisting = false}) {
if (beforeExisting) {
_patches!.insert(0, patch);
} else {
_patches!.add(patch);
}
}
/// If [migrateDependencies] is enabled, any dynamic imports within
/// this [node] will be migrated before continuing.
@override
visitImportRule(ImportRule node) {
super.visitImportRule(node);
if (migrateDependencies) {
for (var import in node.imports) {
if (import is DynamicImport) {
visitDependency(Uri.parse(import.url), import.span, forImport: true);
}
}
}
}
/// If [migrateDependencies] is enabled, this dependency will be
/// migrated before continuing.
@override
visitUseRule(UseRule node) {
super.visitUseRule(node);
if (migrateDependencies) {
visitDependency(node.url, node.span);
}
}
}