|
| 1 | +// Copyright 2021 Google LLC |
| 2 | +// |
| 3 | +// Use of this source code is governed by an MIT-style |
| 4 | +// license that can be found in the LICENSE file or at |
| 5 | +// https://opensource.org/licenses/MIT. |
| 6 | + |
| 7 | +import 'package:args/args.dart'; |
| 8 | +import 'package:sass/sass.dart'; |
| 9 | +import 'package:source_span/source_span.dart'; |
| 10 | + |
| 11 | +// The sass package's API is not necessarily stable. It is being imported with |
| 12 | +// the Sass team's explicit knowledge and approval. See |
| 13 | +// https://github.com/sass/dart-sass/issues/236. |
| 14 | +import 'package:sass/src/ast/sass.dart'; |
| 15 | +import 'package:sass/src/exception.dart'; |
| 16 | +import 'package:sass/src/import_cache.dart'; |
| 17 | + |
| 18 | +import '../migration_visitor.dart'; |
| 19 | +import '../migrator.dart'; |
| 20 | +import '../patch.dart'; |
| 21 | +import '../utils.dart'; |
| 22 | +import '../renamer.dart'; |
| 23 | + |
| 24 | +/// Changes namespaces for `@use` rules within the file(s) being migrated. |
| 25 | +class NamespaceMigrator extends Migrator { |
| 26 | + final name = "namespace"; |
| 27 | + final description = "Change namespaces for `@use` rules."; |
| 28 | + |
| 29 | + @override |
| 30 | + final argParser = ArgParser() |
| 31 | + ..addMultiOption('rename', |
| 32 | + abbr: 'r', |
| 33 | + splitCommas: false, |
| 34 | + help: 'e.g. "old-namespace to new-namespace" or\n' |
| 35 | + ' "url my/url to new-namespace"\n' |
| 36 | + 'See https://sass-lang.com/documentation/cli/migrator#rename.') |
| 37 | + ..addFlag('force', |
| 38 | + abbr: 'f', |
| 39 | + help: 'Force rename namespaces, adding numerical suffixes for ' |
| 40 | + 'conflicts.'); |
| 41 | + |
| 42 | + @override |
| 43 | + Map<Uri, String> migrateFile( |
| 44 | + ImportCache importCache, Stylesheet stylesheet, Importer importer) { |
| 45 | + var renamer = Renamer<UseRule>(argResults['rename'].join('\n'), |
| 46 | + {'': (rule) => rule.namespace, 'url': (rule) => rule.url.toString()}, |
| 47 | + sourceUrl: '--rename'); |
| 48 | + var visitor = _NamespaceMigrationVisitor( |
| 49 | + renamer, argResults['force'] as bool, importCache, migrateDependencies); |
| 50 | + var result = visitor.run(stylesheet, importer); |
| 51 | + missingDependencies.addAll(visitor.missingDependencies); |
| 52 | + return result; |
| 53 | + } |
| 54 | +} |
| 55 | + |
| 56 | +class _NamespaceMigrationVisitor extends MigrationVisitor { |
| 57 | + final Renamer<UseRule> renamer; |
| 58 | + final bool forceRename; |
| 59 | + |
| 60 | + /// A set of spans for each *original* namespace in the current file. |
| 61 | + /// |
| 62 | + /// Each span covers just the namespace of a member reference. |
| 63 | + Map<String, Set<FileSpan>> _spansByNamespace; |
| 64 | + |
| 65 | + /// The set of namespaces used in the current file *after* renaming. |
| 66 | + Set<String> _usedNamespaces; |
| 67 | + |
| 68 | + _NamespaceMigrationVisitor(this.renamer, this.forceRename, |
| 69 | + ImportCache importCache, bool migrateDependencies) |
| 70 | + : super(importCache, migrateDependencies); |
| 71 | + |
| 72 | + @override |
| 73 | + void visitStylesheet(Stylesheet node) { |
| 74 | + var oldSpansByNamespace = _spansByNamespace; |
| 75 | + var oldUsedNamespaces = _usedNamespaces; |
| 76 | + _spansByNamespace = {}; |
| 77 | + _usedNamespaces = {}; |
| 78 | + super.visitStylesheet(node); |
| 79 | + _spansByNamespace = oldSpansByNamespace; |
| 80 | + _usedNamespaces = oldUsedNamespaces; |
| 81 | + } |
| 82 | + |
| 83 | + @override |
| 84 | + void beforePatch(Stylesheet node) { |
| 85 | + // Pass each `@use` rule through the renamer. |
| 86 | + var newNamespaces = <String, Set<UseRule>>{}; |
| 87 | + for (var rule in node.children.whereType<UseRule>()) { |
| 88 | + if (rule.namespace == null) continue; |
| 89 | + newNamespaces |
| 90 | + .putIfAbsent(renamer.rename(rule) ?? rule.namespace, () => {}) |
| 91 | + .add(rule); |
| 92 | + } |
| 93 | + |
| 94 | + // Goes through each new namespace, resolving conflicts if necessary. |
| 95 | + for (var entry in newNamespaces.entries) { |
| 96 | + var newNamespace = entry.key; |
| 97 | + var rules = entry.value; |
| 98 | + if (rules.length == 1) { |
| 99 | + _patchNamespace(rules.first, newNamespace); |
| 100 | + continue; |
| 101 | + } |
| 102 | + |
| 103 | + // If there's still a conflict, fail unless --force is passed. |
| 104 | + if (!forceRename) { |
| 105 | + throw MultiSpanSassException( |
| 106 | + 'Rename failed. ${rules.length} rules would use namespace ' |
| 107 | + '"$newNamespace".\n' |
| 108 | + 'Run with --force to rename with numerical suffixes.', |
| 109 | + rules.first.span, |
| 110 | + '', |
| 111 | + {for (var rule in rules.skip(1)) rule.span: ''}); |
| 112 | + } |
| 113 | + |
| 114 | + // With --force, give the first rule its preferred namespace and then |
| 115 | + // add numerical suffixes to the rest. |
| 116 | + var suffix = 2; |
| 117 | + for (var rule in rules) { |
| 118 | + var forcedNamespace = newNamespace; |
| 119 | + while (_usedNamespaces.contains(forcedNamespace)) { |
| 120 | + forcedNamespace = '$newNamespace$suffix'; |
| 121 | + suffix++; |
| 122 | + } |
| 123 | + _patchNamespace(rule, forcedNamespace); |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + /// Patch [rule] and all references to it with [newNamespace]. |
| 129 | + void _patchNamespace(UseRule rule, String newNamespace) { |
| 130 | + _usedNamespaces.add(newNamespace); |
| 131 | + if (rule.namespace == newNamespace) return; |
| 132 | + var asClause = |
| 133 | + RegExp('\\s*as\\s+(${rule.namespace})').firstMatch(rule.span.text); |
| 134 | + if (asClause == null) { |
| 135 | + // Add an `as` clause to a rule that previously lacked one. |
| 136 | + var end = RegExp(r"""@use\s("|').*?\1""").firstMatch(rule.span.text).end; |
| 137 | + addPatch( |
| 138 | + Patch.insert(rule.span.subspan(0, end).end, ' as $newNamespace')); |
| 139 | + } else if (namespaceForPath(rule.url.toString()) == newNamespace) { |
| 140 | + // Remove an `as` clause that is no longer necessary. |
| 141 | + addPatch( |
| 142 | + patchDelete(rule.span, start: asClause.start, end: asClause.end)); |
| 143 | + } else { |
| 144 | + // Change the namespace of an existing `as` clause. |
| 145 | + addPatch(Patch( |
| 146 | + rule.span.subspan(asClause.end - rule.namespace.length, asClause.end), |
| 147 | + newNamespace)); |
| 148 | + } |
| 149 | + for (FileSpan span in _spansByNamespace[rule.namespace] ?? {}) { |
| 150 | + addPatch(Patch(span, newNamespace)); |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + /// If [namespace] is not null, add its span to [_spansByNamespace]. |
| 155 | + void _addNamespaceSpan(String namespace, FileSpan span) { |
| 156 | + if (namespace != null) { |
| 157 | + assert(span.text.startsWith(namespace)); |
| 158 | + _spansByNamespace |
| 159 | + .putIfAbsent(namespace, () => {}) |
| 160 | + .add(subspan(span, end: namespace.length)); |
| 161 | + } |
| 162 | + } |
| 163 | + |
| 164 | + @override |
| 165 | + void visitFunctionExpression(FunctionExpression node) { |
| 166 | + _addNamespaceSpan(node.namespace, node.span); |
| 167 | + var name = node.name.asPlain; |
| 168 | + if (name == 'get-function') { |
| 169 | + var moduleArg = node.arguments.named['module']; |
| 170 | + if (node.arguments.positional.length == 3) { |
| 171 | + moduleArg ??= node.arguments.positional[2]; |
| 172 | + } |
| 173 | + if (moduleArg is StringExpression) { |
| 174 | + var namespace = moduleArg.text.asPlain; |
| 175 | + if (namespace != null) { |
| 176 | + var span = moduleArg.hasQuotes |
| 177 | + ? moduleArg.span.subspan(1, moduleArg.span.length - 1) |
| 178 | + : moduleArg.span; |
| 179 | + _addNamespaceSpan(namespace, span); |
| 180 | + } |
| 181 | + } |
| 182 | + } |
| 183 | + super.visitFunctionExpression(node); |
| 184 | + } |
| 185 | + |
| 186 | + @override |
| 187 | + void visitIncludeRule(IncludeRule node) { |
| 188 | + if (node.namespace != null) { |
| 189 | + var startNamespace = node.span.text.indexOf( |
| 190 | + node.namespace, node.span.text[0] == '+' ? 1 : '@include'.length); |
| 191 | + _addNamespaceSpan(node.namespace, node.span.subspan(startNamespace)); |
| 192 | + } |
| 193 | + super.visitIncludeRule(node); |
| 194 | + } |
| 195 | + |
| 196 | + @override |
| 197 | + void visitVariableDeclaration(VariableDeclaration node) { |
| 198 | + _addNamespaceSpan(node.namespace, node.span); |
| 199 | + super.visitVariableDeclaration(node); |
| 200 | + } |
| 201 | + |
| 202 | + @override |
| 203 | + void visitVariableExpression(VariableExpression node) { |
| 204 | + _addNamespaceSpan(node.namespace, node.span); |
| 205 | + super.visitVariableExpression(node); |
| 206 | + } |
| 207 | +} |
0 commit comments