Skip to content

Commit c27792b

Browse files
authored
Add legacy color function migrator (#260)
1 parent dce67db commit c27792b

File tree

11 files changed

+352
-7
lines changed

11 files changed

+352
-7
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 2.1.0
2+
3+
### Color Function Migrator
4+
5+
* Adds a new `color` migrator that migrates off of legacy color functions that
6+
were deprecated in Dart Sass 1.79.0.
7+
18
## 2.0.3
29

310
### Module Migrator

lib/src/migrators/color.dart

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// Copyright 2024 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:sass_api/sass_api.dart';
8+
import 'package:sass_migrator/src/migrators/module/reference_source.dart';
9+
import 'package:source_span/source_span.dart';
10+
11+
import 'module/references.dart';
12+
import '../migration_visitor.dart';
13+
import '../migrator.dart';
14+
import '../patch.dart';
15+
import '../utils.dart';
16+
17+
/// Migrates off of legacy color functions.
18+
class ColorMigrator extends Migrator {
19+
final name = "color";
20+
final description = "Migrates off of legacy color functions.";
21+
22+
@override
23+
Map<Uri, String> migrateFile(
24+
ImportCache importCache, Stylesheet stylesheet, Importer importer) {
25+
var references = References(importCache, stylesheet, importer);
26+
var visitor =
27+
_ColorMigrationVisitor(references, importCache, migrateDependencies);
28+
var result = visitor.run(stylesheet, importer);
29+
missingDependencies.addAll(visitor.missingDependencies);
30+
return result;
31+
}
32+
}
33+
34+
/// URL for the sass:color module.
35+
final _colorUrl = Uri(scheme: 'sass', path: 'color');
36+
37+
class _ColorMigrationVisitor extends MigrationVisitor {
38+
final References references;
39+
40+
_ColorMigrationVisitor(
41+
this.references, super.importCache, super.migrateDependencies);
42+
43+
/// The namespace of an existing `@use "sass:color"` rule in the current
44+
/// file, if any.
45+
String? _colorModuleNamespace;
46+
47+
/// The set of all other namespaces already used in the current file.
48+
Set<String> _usedNamespaces = {};
49+
50+
@override
51+
void visitStylesheet(Stylesheet node) {
52+
var oldColorModuleNamespace = _colorModuleNamespace;
53+
var oldUsedNamespaces = _usedNamespaces;
54+
_colorModuleNamespace = null;
55+
_usedNamespaces = {};
56+
// Check all the namespaces used by this file before visiting the
57+
// stylesheet, in case deprecated functions are called before all `@use`
58+
// rules.
59+
for (var useRule in node.uses) {
60+
if (_colorModuleNamespace != null || useRule.namespace == null) continue;
61+
if (useRule.url == _colorUrl) {
62+
_colorModuleNamespace = useRule.namespace;
63+
} else {
64+
_usedNamespaces.add(useRule.namespace!);
65+
}
66+
}
67+
super.visitStylesheet(node);
68+
_colorModuleNamespace = oldColorModuleNamespace;
69+
_usedNamespaces = oldUsedNamespaces;
70+
}
71+
72+
@override
73+
void visitFunctionExpression(FunctionExpression node) {
74+
var source = references.sources[node];
75+
if (source is! BuiltInSource || source.url != _colorUrl) return;
76+
switch (node.name) {
77+
case 'red' || 'green' || 'blue':
78+
_patchChannel(node, 'rgb');
79+
case 'hue' || 'saturation' || 'lightness':
80+
_patchChannel(node, 'hsl');
81+
case 'whiteness' || 'blackness':
82+
_patchChannel(node, 'hwb');
83+
case 'alpha':
84+
_patchChannel(node);
85+
case 'adjust-hue':
86+
_patchAdjust(node, channel: 'hue', space: 'hsl');
87+
case 'saturate'
88+
when node.arguments.named.length + node.arguments.positional.length !=
89+
1:
90+
_patchAdjust(node, channel: 'saturation', space: 'hsl');
91+
case 'desaturate':
92+
_patchAdjust(node, channel: 'saturation', negate: true, space: 'hsl');
93+
case 'transparentize' || 'fade-out':
94+
_patchAdjust(node, channel: 'alpha', negate: true);
95+
case 'opacify' || 'fade-in':
96+
_patchAdjust(node, channel: 'alpha');
97+
case 'lighten':
98+
_patchAdjust(node, channel: 'lightness', space: 'hsl');
99+
case 'darken':
100+
_patchAdjust(node, channel: 'lightness', negate: true, space: 'hsl');
101+
default:
102+
return;
103+
}
104+
if (node.namespace == null) {
105+
addPatch(
106+
patchBefore(
107+
node, '${_getOrAddColorModuleNamespace(node.span.file)}.'),
108+
beforeExisting: true);
109+
}
110+
}
111+
112+
/// Returns the namespace used for the color module, adding a new `@use` rule
113+
/// if necessary.
114+
String _getOrAddColorModuleNamespace(SourceFile file) {
115+
if (_colorModuleNamespace == null) {
116+
_colorModuleNamespace = _chooseColorModuleNamespace();
117+
var asClause =
118+
_colorModuleNamespace == 'color' ? '' : ' as $_colorModuleNamespace';
119+
addPatch(
120+
Patch.insert(file.location(0), '@use "sass:color"$asClause;\n\n'));
121+
}
122+
return _colorModuleNamespace!;
123+
}
124+
125+
/// Find an unused namespace for the sass:color module.
126+
String _chooseColorModuleNamespace() {
127+
if (!_usedNamespaces.contains('color')) return 'color';
128+
if (!_usedNamespaces.contains('sass-color')) return 'sass-color';
129+
var count = 2;
130+
var namespace = 'color$count';
131+
while (_usedNamespaces.contains(namespace)) {
132+
namespace = 'color${++count}';
133+
}
134+
return namespace;
135+
}
136+
137+
/// Patches a deprecated channel function to use `color.channel` instead.
138+
void _patchChannel(FunctionExpression node, [String? colorSpace]) {
139+
addPatch(Patch(node.nameSpan, 'channel'));
140+
141+
if (node.arguments.named.isEmpty) {
142+
addPatch(patchAfter(
143+
node.arguments.positional.last,
144+
", '${node.name}'"
145+
"${colorSpace == null ? '' : ', \$space: $colorSpace'}"));
146+
} else {
147+
addPatch(patchAfter(
148+
[...node.arguments.positional, ...node.arguments.named.values].last,
149+
", \$channel: '${node.name}'"
150+
"${colorSpace == null ? '' : ', \$space: $colorSpace'}"));
151+
}
152+
}
153+
154+
/// Patches a deprecated adjustment function to use `color.adjust` instead.
155+
void _patchAdjust(FunctionExpression node,
156+
{required String channel, bool negate = false, String? space}) {
157+
addPatch(Patch(node.nameSpan, 'adjust'));
158+
switch (node.arguments) {
159+
case ArgumentInvocation(positional: [_, var adjustment]):
160+
addPatch(patchBefore(adjustment, '\$$channel: ${negate ? '-' : ''}'));
161+
if (negate && adjustment.needsParens) {
162+
addPatch(patchBefore(adjustment, '('));
163+
addPatch(patchAfter(adjustment, ')'));
164+
}
165+
if (space != null) {
166+
addPatch(patchAfter(adjustment, ', \$space: $space'));
167+
}
168+
169+
case ArgumentInvocation(
170+
named: {'amount': var adjustment} || {'degrees': var adjustment}
171+
):
172+
var start = adjustment.span.start.offset - 1;
173+
while (adjustment.span.file.getText(start, start + 1) != r'$') {
174+
start--;
175+
}
176+
var argNameSpan = adjustment.span.file
177+
.location(start + 1)
178+
.pointSpan()
179+
.extendIfMatches('amount')
180+
.extendIfMatches('degrees');
181+
addPatch(Patch(argNameSpan, channel));
182+
if (negate) {
183+
addPatch(patchBefore(adjustment, '-'));
184+
if (adjustment.needsParens) {
185+
addPatch(patchBefore(adjustment, '('));
186+
addPatch(patchAfter(adjustment, ')'));
187+
}
188+
}
189+
if (space != null) {
190+
addPatch(patchAfter(adjustment, ', \$space: $space'));
191+
}
192+
193+
default:
194+
warn(node.span.message('Cannot migrate unexpected arguments.'));
195+
}
196+
}
197+
}
198+
199+
extension _NeedsParens on Expression {
200+
/// Returns true if this expression needs parentheses when it's negated.
201+
bool get needsParens => switch (this) {
202+
BinaryOperationExpression() ||
203+
UnaryOperationExpression() ||
204+
FunctionExpression() =>
205+
true,
206+
_ => false,
207+
};
208+
}

lib/src/runner.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'dart:isolate';
99
import 'package:args/args.dart';
1010
import 'package:args/command_runner.dart';
1111
import 'package:path/path.dart' as p;
12+
import 'package:sass_migrator/src/migrators/color.dart';
1213
import 'package:source_span/source_span.dart';
1314
import 'package:term_glyph/term_glyph.dart' as glyph;
1415

@@ -55,6 +56,7 @@ class MigratorRunner extends CommandRunner<Map<Uri, String>> {
5556
..addFlag('version',
5657
help: 'Print the version of the Sass migrator.', negatable: false);
5758
addCommand(CalculationInterpolationMigrator());
59+
addCommand(ColorMigrator());
5860
addCommand(DivisionMigrator());
5961
addCommand(ModuleMigrator());
6062
addCommand(NamespaceMigrator());

pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: sass_migrator
2-
version: 2.0.3
2+
version: 2.1.0
33
description: A tool for running migrations on Sass files
44
homepage: https://github.com/sass/migrator
55

@@ -17,7 +17,7 @@ dependencies:
1717
node_interop: ^2.0.2
1818
node_io: ^2.3.0
1919
path: ^1.8.0
20-
sass_api: ^9.2.7
20+
sass_api: ^12.0.0
2121
source_span: ^1.8.1
2222
stack_trace: ^1.10.0
2323
string_scanner: ^1.1.0

test/migrators/calc_interpolation/calc_remove_interpolation.hrx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ $d: 5;
88
.a { .b: calc($b * #{$c + 1}); }
99

1010
// More than one interpolations
11-
.a {
12-
.b: calc($b - #{$c + 1} + #{$d});
11+
.a {
12+
.b: calc($b - #{$c + 1} + #{$d});
1313
.c: calc(100% - #{$TABLE_TITLE + 2px});
1414
}
1515

@@ -35,9 +35,9 @@ $d: 5;
3535
.a { .b: calc($b * ($c + 1)); }
3636

3737
// More than one interpolations
38-
.a {
39-
.b: calc($b - ($c + 1) + $d);
40-
.c: calc(100% - ($TABLE-TITLE + 2px));
38+
.a {
39+
.b: calc($b - ($c + 1) + $d);
40+
.c: calc(100% - ($TABLE_TITLE + 2px));
4141
}
4242

4343
// Nested

test/migrators/color/global.hrx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<==> input/entrypoint.scss
2+
a {
3+
b: red(gold);
4+
c: green(gold);
5+
d: blue(gold);
6+
e: hue(gold);
7+
f: saturation(gold);
8+
g: lightness(gold);
9+
h: adjust-hue(gold, 20deg);
10+
i: saturate(gold, 10%);
11+
j: desaturate(gold, 10%);
12+
k: transparentize(gold, 0.1);
13+
l: fade-out(gold, 0.1);
14+
m: opacify(gold, 0.1);
15+
n: fade-in(gold, 0.1);
16+
o: lighten(gold, 10%);
17+
p: darken(gold, 10%);
18+
}
19+
20+
<==> output/entrypoint.scss
21+
@use "sass:color";
22+
23+
a {
24+
b: color.channel(gold, 'red', $space: rgb);
25+
c: color.channel(gold, 'green', $space: rgb);
26+
d: color.channel(gold, 'blue', $space: rgb);
27+
e: color.channel(gold, 'hue', $space: hsl);
28+
f: color.channel(gold, 'saturation', $space: hsl);
29+
g: color.channel(gold, 'lightness', $space: hsl);
30+
h: color.adjust(gold, $hue: 20deg, $space: hsl);
31+
i: color.adjust(gold, $saturation: 10%, $space: hsl);
32+
j: color.adjust(gold, $saturation: -10%, $space: hsl);
33+
k: color.adjust(gold, $alpha: -0.1);
34+
l: color.adjust(gold, $alpha: -0.1);
35+
m: color.adjust(gold, $alpha: 0.1);
36+
n: color.adjust(gold, $alpha: 0.1);
37+
o: color.adjust(gold, $lightness: 10%, $space: hsl);
38+
p: color.adjust(gold, $lightness: -10%, $space: hsl);
39+
}

test/migrators/color/module.hrx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<==> input/entrypoint.scss
2+
@use "sass:color";
3+
4+
a {
5+
b: color.red(gold);
6+
c: color.green(gold);
7+
d: color.blue(gold);
8+
e: color.hue(gold);
9+
f: color.saturation(gold);
10+
g: color.lightness(gold);
11+
h: color.whiteness(gold);
12+
i: color.blackness(gold);
13+
j: color.alpha(gold);
14+
}
15+
16+
<==> output/entrypoint.scss
17+
@use "sass:color";
18+
19+
a {
20+
b: color.channel(gold, 'red', $space: rgb);
21+
c: color.channel(gold, 'green', $space: rgb);
22+
d: color.channel(gold, 'blue', $space: rgb);
23+
e: color.channel(gold, 'hue', $space: hsl);
24+
f: color.channel(gold, 'saturation', $space: hsl);
25+
g: color.channel(gold, 'lightness', $space: hsl);
26+
h: color.channel(gold, 'whiteness', $space: hwb);
27+
i: color.channel(gold, 'blackness', $space: hwb);
28+
j: color.channel(gold, 'alpha');
29+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<==> input/entrypoint.scss
2+
a {
3+
b: red($color: gold);
4+
c: adjust-hue(gold, $degrees: 20deg);
5+
d: saturate(gold, $amount: 10%);
6+
e: desaturate($color: gold, $amount: 10%);
7+
f: lighten($amount: 10%, $color: gold);
8+
}
9+
10+
<==> output/entrypoint.scss
11+
@use "sass:color";
12+
13+
a {
14+
b: color.channel($color: gold, $channel: 'red', $space: rgb);
15+
c: color.adjust(gold, $hue: 20deg, $space: hsl);
16+
d: color.adjust(gold, $saturation: 10%, $space: hsl);
17+
e: color.adjust($color: gold, $saturation: -10%, $space: hsl);
18+
f: color.adjust($lightness: 10%, $space: hsl, $color: gold);
19+
}

test/migrators/color/namespace.hrx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<==> input/entrypoint.scss
2+
@use "sass:math" as color;
3+
4+
a {
5+
b: red(gold);
6+
c: green(gold);
7+
d: blue(gold);
8+
}
9+
10+
<==> output/entrypoint.scss
11+
@use "sass:color" as sass-color;
12+
13+
@use "sass:math" as color;
14+
15+
a {
16+
b: sass-color.channel(gold, 'red', $space: rgb);
17+
c: sass-color.channel(gold, 'green', $space: rgb);
18+
d: sass-color.channel(gold, 'blue', $space: rgb);
19+
}

0 commit comments

Comments
 (0)