Skip to content

Commit 821b98e

Browse files
authored
Don't cache canonicalize calls when containingUrl is available (#2215)
See #2208
1 parent c5aff1b commit 821b98e

File tree

6 files changed

+124
-65
lines changed

6 files changed

+124
-65
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## 1.75.0
2+
3+
* Fix a bug in which stylesheet canonicalization could be cached incorrectly
4+
when custom importers or the Node.js package importer made decisions based on
5+
the URL of the containing stylesheet.
6+
7+
### JS API
8+
9+
* Allow `importer` to be passed without `url` in `StringOptionsWithImporter`.
10+
111
## 1.74.1
212

313
* No user-visible changes.

lib/src/async_import_cache.dart

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -154,64 +154,85 @@ final class AsyncImportCache {
154154
}
155155

156156
if (baseImporter != null && url.scheme == '') {
157-
var relativeResult = await putIfAbsentAsync(
158-
_relativeCanonicalizeCache,
159-
(
160-
url,
161-
forImport: forImport,
162-
baseImporter: baseImporter,
163-
baseUrl: baseUrl
164-
),
165-
() => _canonicalize(baseImporter, baseUrl?.resolveUri(url) ?? url,
166-
baseUrl, forImport));
157+
var relativeResult = await putIfAbsentAsync(_relativeCanonicalizeCache, (
158+
url,
159+
forImport: forImport,
160+
baseImporter: baseImporter,
161+
baseUrl: baseUrl
162+
), () async {
163+
var (result, cacheable) = await _canonicalize(
164+
baseImporter, baseUrl?.resolveUri(url) ?? url, baseUrl, forImport);
165+
assert(
166+
cacheable,
167+
"Relative loads should always be cacheable because they never "
168+
"provide access to the containing URL.");
169+
return result;
170+
});
167171
if (relativeResult != null) return relativeResult;
168172
}
169173

170-
return await putIfAbsentAsync(
171-
_canonicalizeCache, (url, forImport: forImport), () async {
172-
for (var importer in _importers) {
173-
if (await _canonicalize(importer, url, baseUrl, forImport)
174-
case var result?) {
174+
var key = (url, forImport: forImport);
175+
if (_canonicalizeCache.containsKey(key)) return _canonicalizeCache[key];
176+
177+
// Each indivudal call to a `canonicalize()` override may not be cacheable
178+
// (specifically, if it has access to `containingUrl` it's too
179+
// context-sensitive to usefully cache). We want to cache a given URL across
180+
// the _entire_ importer chain, so we use [cacheable] to track whether _all_
181+
// `canonicalize()` calls we've attempted are cacheable. Only if they are do
182+
// we store the result in the cache.
183+
var cacheable = true;
184+
for (var importer in _importers) {
185+
switch (await _canonicalize(importer, url, baseUrl, forImport)) {
186+
case (var result?, true) when cacheable:
187+
_canonicalizeCache[key] = result;
188+
return result;
189+
190+
case (var result?, _):
175191
return result;
176-
}
192+
193+
case (_, false):
194+
cacheable = false;
177195
}
196+
}
178197

179-
return null;
180-
});
198+
if (cacheable) _canonicalizeCache[key] = null;
199+
return null;
181200
}
182201

183202
/// Calls [importer.canonicalize] and prints a deprecation warning if it
184203
/// returns a relative URL.
185204
///
186-
/// If [resolveUrl] is `true`, this resolves [url] relative to [baseUrl]
187-
/// before passing it to [importer].
188-
Future<AsyncCanonicalizeResult?> _canonicalize(
189-
AsyncImporter importer, Uri url, Uri? baseUrl, bool forImport,
190-
{bool resolveUrl = false}) async {
191-
var resolved =
192-
resolveUrl && baseUrl != null ? baseUrl.resolveUri(url) : url;
205+
/// This returns both the result of the call to `canonicalize()` and whether
206+
/// that result is cacheable at all.
207+
Future<(AsyncCanonicalizeResult?, bool cacheable)> _canonicalize(
208+
AsyncImporter importer, Uri url, Uri? baseUrl, bool forImport) async {
193209
var canonicalize = forImport
194-
? () => inImportRule(() => importer.canonicalize(resolved))
195-
: () => importer.canonicalize(resolved);
210+
? () => inImportRule(() => importer.canonicalize(url))
211+
: () => importer.canonicalize(url);
196212

197213
var passContainingUrl = baseUrl != null &&
198214
(url.scheme == '' || await importer.isNonCanonicalScheme(url.scheme));
199215
var result = await withContainingUrl(
200216
passContainingUrl ? baseUrl : null, canonicalize);
201-
if (result == null) return null;
217+
218+
// TODO(sass/dart-sass#2208): Determine whether the containing URL was
219+
// _actually_ accessed rather than assuming it was.
220+
var cacheable = !passContainingUrl || importer is FilesystemImporter;
221+
222+
if (result == null) return (null, cacheable);
202223

203224
if (result.scheme == '') {
204225
_logger.warnForDeprecation(
205226
Deprecation.relativeCanonical,
206-
"Importer $importer canonicalized $resolved to $result.\n"
227+
"Importer $importer canonicalized $url to $result.\n"
207228
"Relative canonical URLs are deprecated and will eventually be "
208229
"disallowed.");
209230
} else if (await importer.isNonCanonicalScheme(result.scheme)) {
210-
throw "Importer $importer canonicalized $resolved to $result, which "
211-
"uses a scheme declared as non-canonical.";
231+
throw "Importer $importer canonicalized $url to $result, which uses a "
232+
"scheme declared as non-canonical.";
212233
}
213234

214-
return (importer, result, originalUrl: resolved);
235+
return ((importer, result, originalUrl: url), cacheable);
215236
}
216237

217238
/// Tries to import [url] using one of this cache's importers.

lib/src/import_cache.dart

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// DO NOT EDIT. This file was generated from async_import_cache.dart.
66
// See tool/grind/synchronize.dart for details.
77
//
8-
// Checksum: d157b83599dbc07a80ac6cb5ffdf5dde03b60376
8+
// Checksum: 37dd173d676ec6cf201a25b3cca9ac81d92b1433
99
//
1010
// ignore_for_file: unused_import
1111

@@ -154,61 +154,85 @@ final class ImportCache {
154154
}
155155

156156
if (baseImporter != null && url.scheme == '') {
157-
var relativeResult = _relativeCanonicalizeCache.putIfAbsent(
158-
(
159-
url,
160-
forImport: forImport,
161-
baseImporter: baseImporter,
162-
baseUrl: baseUrl
163-
),
164-
() => _canonicalize(baseImporter, baseUrl?.resolveUri(url) ?? url,
165-
baseUrl, forImport));
157+
var relativeResult = _relativeCanonicalizeCache.putIfAbsent((
158+
url,
159+
forImport: forImport,
160+
baseImporter: baseImporter,
161+
baseUrl: baseUrl
162+
), () {
163+
var (result, cacheable) = _canonicalize(
164+
baseImporter, baseUrl?.resolveUri(url) ?? url, baseUrl, forImport);
165+
assert(
166+
cacheable,
167+
"Relative loads should always be cacheable because they never "
168+
"provide access to the containing URL.");
169+
return result;
170+
});
166171
if (relativeResult != null) return relativeResult;
167172
}
168173

169-
return _canonicalizeCache.putIfAbsent((url, forImport: forImport), () {
170-
for (var importer in _importers) {
171-
if (_canonicalize(importer, url, baseUrl, forImport) case var result?) {
174+
var key = (url, forImport: forImport);
175+
if (_canonicalizeCache.containsKey(key)) return _canonicalizeCache[key];
176+
177+
// Each indivudal call to a `canonicalize()` override may not be cacheable
178+
// (specifically, if it has access to `containingUrl` it's too
179+
// context-sensitive to usefully cache). We want to cache a given URL across
180+
// the _entire_ importer chain, so we use [cacheable] to track whether _all_
181+
// `canonicalize()` calls we've attempted are cacheable. Only if they are do
182+
// we store the result in the cache.
183+
var cacheable = true;
184+
for (var importer in _importers) {
185+
switch (_canonicalize(importer, url, baseUrl, forImport)) {
186+
case (var result?, true) when cacheable:
187+
_canonicalizeCache[key] = result;
188+
return result;
189+
190+
case (var result?, _):
172191
return result;
173-
}
192+
193+
case (_, false):
194+
cacheable = false;
174195
}
196+
}
175197

176-
return null;
177-
});
198+
if (cacheable) _canonicalizeCache[key] = null;
199+
return null;
178200
}
179201

180202
/// Calls [importer.canonicalize] and prints a deprecation warning if it
181203
/// returns a relative URL.
182204
///
183-
/// If [resolveUrl] is `true`, this resolves [url] relative to [baseUrl]
184-
/// before passing it to [importer].
185-
CanonicalizeResult? _canonicalize(
186-
Importer importer, Uri url, Uri? baseUrl, bool forImport,
187-
{bool resolveUrl = false}) {
188-
var resolved =
189-
resolveUrl && baseUrl != null ? baseUrl.resolveUri(url) : url;
205+
/// This returns both the result of the call to `canonicalize()` and whether
206+
/// that result is cacheable at all.
207+
(CanonicalizeResult?, bool cacheable) _canonicalize(
208+
Importer importer, Uri url, Uri? baseUrl, bool forImport) {
190209
var canonicalize = forImport
191-
? () => inImportRule(() => importer.canonicalize(resolved))
192-
: () => importer.canonicalize(resolved);
210+
? () => inImportRule(() => importer.canonicalize(url))
211+
: () => importer.canonicalize(url);
193212

194213
var passContainingUrl = baseUrl != null &&
195214
(url.scheme == '' || importer.isNonCanonicalScheme(url.scheme));
196215
var result =
197216
withContainingUrl(passContainingUrl ? baseUrl : null, canonicalize);
198-
if (result == null) return null;
217+
218+
// TODO(sass/dart-sass#2208): Determine whether the containing URL was
219+
// _actually_ accessed rather than assuming it was.
220+
var cacheable = !passContainingUrl || importer is FilesystemImporter;
221+
222+
if (result == null) return (null, cacheable);
199223

200224
if (result.scheme == '') {
201225
_logger.warnForDeprecation(
202226
Deprecation.relativeCanonical,
203-
"Importer $importer canonicalized $resolved to $result.\n"
227+
"Importer $importer canonicalized $url to $result.\n"
204228
"Relative canonical URLs are deprecated and will eventually be "
205229
"disallowed.");
206230
} else if (importer.isNonCanonicalScheme(result.scheme)) {
207-
throw "Importer $importer canonicalized $resolved to $result, which "
208-
"uses a scheme declared as non-canonical.";
231+
throw "Importer $importer canonicalized $url to $result, which uses a "
232+
"scheme declared as non-canonical.";
209233
}
210234

211-
return (importer, result, originalUrl: resolved);
235+
return ((importer, result, originalUrl: url), cacheable);
212236
}
213237

214238
/// Tries to import [url] using one of this cache's importers.

pkg/sass_api/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 10.2.0
2+
3+
* No user-visible changes.
4+
15
## 10.1.1
26

37
* No user-visible changes.

pkg/sass_api/pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ name: sass_api
22
# Note: Every time we add a new Sass AST node, we need to bump the *major*
33
# version because it's a breaking change for anyone who's implementing the
44
# visitor interface(s).
5-
version: 10.1.1
5+
version: 10.2.0
66
description: Additional APIs for Dart Sass.
77
homepage: https://github.com/sass/dart-sass
88

99
environment:
1010
sdk: ">=3.0.0 <4.0.0"
1111

1212
dependencies:
13-
sass: 1.74.1
13+
sass: 1.75.0
1414

1515
dev_dependencies:
1616
dartdoc: ^6.0.0

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: sass
2-
version: 1.74.1
2+
version: 1.75.0
33
description: A Sass implementation in Dart.
44
homepage: https://github.com/sass/dart-sass
55

0 commit comments

Comments
 (0)