diff --git a/lib/src/async_import_cache.dart b/lib/src/async_import_cache.dart index e2ceee46..dc50aeaa 100644 --- a/lib/src/async_import_cache.dart +++ b/lib/src/async_import_cache.dart @@ -150,4 +150,12 @@ class AsyncImportCache { url: displayUrl, logger: _logger); }); } + + /// Clears the cached parse tree for the stylesheet with the given + /// [canonicalUrl]. + /// + /// Has no effect if the imported file at [canonicalUrl] has not been cached. + void clearImport(Uri canonicalUrl) { + _importCache.remove(canonicalUrl); + } } diff --git a/lib/src/import_cache.dart b/lib/src/import_cache.dart index 55287d60..4e16090c 100644 --- a/lib/src/import_cache.dart +++ b/lib/src/import_cache.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_import_cache.dart. // See tool/synchronize.dart for details. // -// Checksum: a4cda7503f4d6276fa18bf77306d9e58e0901ced +// Checksum: 90302aba01c574ffbc07e75a34606406e48df7fc import 'package:tuple/tuple.dart'; @@ -153,4 +153,12 @@ class ImportCache { url: displayUrl, logger: _logger); }); } + + /// Clears the cached parse tree for the stylesheet with the given + /// [canonicalUrl]. + /// + /// Has no effect if the imported file at [canonicalUrl] has not been cached. + void clearImport(Uri canonicalUrl) { + _importCache.remove(canonicalUrl); + } } diff --git a/lib/src/stylesheet_graph.dart b/lib/src/stylesheet_graph.dart index 060338e7..ee41cba9 100644 --- a/lib/src/stylesheet_graph.dart +++ b/lib/src/stylesheet_graph.dart @@ -4,15 +4,19 @@ import 'dart:collection'; +import 'package:collection/collection.dart'; + import 'ast/sass.dart'; import 'import_cache.dart'; import 'importer.dart'; import 'visitor/find_imports.dart'; -/// A graph of the import relationships between stylesheets. +/// A graph of the import relationships between stylesheets, available via +/// [nodes]. class StylesheetGraph { /// A map from canonical URLs to the stylesheet nodes for those URLs. - final _nodes = {}; + Map get nodes => new UnmodifiableMapView(_nodes); + final _nodes = {}; /// The import cache used to load stylesheets. final ImportCache importCache; @@ -32,7 +36,7 @@ class StylesheetGraph { /// Returns `true` if the import cache can't find a stylesheet at [url]. bool modifiedSince(Uri url, DateTime since, [Importer baseImporter, Uri baseUrl]) { - DateTime transitiveModificationTime(_StylesheetNode node) { + DateTime transitiveModificationTime(StylesheetNode node) { return _transitiveModificationTimes.putIfAbsent(node.canonicalUrl, () { var latest = node.importer.modificationTime(node.canonicalUrl); for (var upstream in node.upstream.values) { @@ -59,29 +63,42 @@ class StylesheetGraph { /// import [url] (resolved relative to [baseUrl] if it's passed). /// /// Returns `null` if the import cache can't find a stylesheet at [url]. - _StylesheetNode _add(Uri url, [Importer baseImporter, Uri baseUrl]) { + StylesheetNode _add(Uri url, [Importer baseImporter, Uri baseUrl]) { var tuple = _ignoreErrors( () => importCache.canonicalize(url, baseImporter, baseUrl)); if (tuple == null) return null; var importer = tuple.item1; var canonicalUrl = tuple.item2; + return addCanonical(importer, canonicalUrl, url); + } + + /// Adds the stylesheet at the canonicalized [canonicalUrl] and all the + /// stylesheets it imports to this graph and returns its node. + /// + /// Returns `null` if [importer] can't import [canonicalUrl]. + /// + /// If passed, the [originalUrl] represents the URL that was canonicalized + /// into [canonicalUrl]. It's used as the URL for the parsed stylesheet, which + /// is in turn used in error reporting. + StylesheetNode addCanonical(Importer importer, Uri canonicalUrl, + [Uri originalUrl]) { return _nodes.putIfAbsent(canonicalUrl, () { - var stylesheet = _ignoreErrors( - () => importCache.importCanonical(importer, canonicalUrl, url)); + var stylesheet = _ignoreErrors(() => + importCache.importCanonical(importer, canonicalUrl, originalUrl)); if (stylesheet == null) return null; - return new _StylesheetNode(stylesheet, importer, canonicalUrl, + return new StylesheetNode._(stylesheet, importer, canonicalUrl, _upstreamNodes(stylesheet, importer, canonicalUrl)); }); } /// Returns a map from non-canonicalized imported URLs in [stylesheet], which /// appears within [baseUrl] imported by [baseImporter]. - Map _upstreamNodes( + Map _upstreamNodes( Stylesheet stylesheet, Importer baseImporter, Uri baseUrl) { var active = new Set.from([baseUrl]); - var upstream = {}; + var upstream = {}; for (var import in findImports(stylesheet)) { var url = Uri.parse(import.url); upstream[url] = _nodeFor(url, baseImporter, baseUrl, active); @@ -89,12 +106,61 @@ class StylesheetGraph { return upstream; } + /// Re-parses the stylesheet at [canonicalUrl] and updates the dependency graph + /// accordingly. + /// + /// Throws a [StateError] if [canonicalUrl] isn't already in the dependency graph. + /// + /// Removes the stylesheet from the graph entirely and returns `null` if the + /// stylesheet's importer can no longer import it. + StylesheetNode reload(Uri canonicalUrl) { + var node = _nodes[canonicalUrl]; + if (node == null) { + throw new StateError("$canonicalUrl is not in the dependency graph."); + } + + // Rather than spending time computing exactly which modification times + // should be updated, just clear the cache and let it be computed again + // later. + _transitiveModificationTimes.clear(); + + importCache.clearImport(canonicalUrl); + var stylesheet = importCache.importCanonical(node.importer, canonicalUrl); + if (stylesheet == null) { + remove(canonicalUrl); + return null; + } + + node._stylesheet = stylesheet; + node._replaceUpstream( + _upstreamNodes(stylesheet, node.importer, canonicalUrl)); + return node; + } + + /// Removes the stylesheet at [canonicalUrl] from the stylesheet graph. + /// + /// Throws a [StateError] if [canonicalUrl] isn't already in the dependency graph. + void remove(Uri canonicalUrl) { + var node = _nodes[canonicalUrl]; + if (node == null) { + throw new StateError("$canonicalUrl is not in the dependency graph."); + } + + // Rather than spending time computing exactly which modification times + // should be updated, just clear the cache and let it be computed again + // later. + _transitiveModificationTimes.clear(); + + importCache.clearImport(canonicalUrl); + node._remove(); + } + /// Returns the [StylesheetNode] for the stylesheet at the given [url], which /// appears within [baseUrl] imported by [baseImporter]. /// /// The [active] set should contain the canonical URLs that are currently /// being imported. It's used to detect circular imports. - _StylesheetNode _nodeFor( + StylesheetNode _nodeFor( Uri url, Importer baseImporter, Uri baseUrl, Set active) { var tuple = _ignoreErrors( () => importCache.canonicalize(url, baseImporter, baseUrl)); @@ -118,7 +184,7 @@ class StylesheetGraph { if (stylesheet == null) return null; active.add(canonicalUrl); - var node = new _StylesheetNode(stylesheet, importer, canonicalUrl, + var node = new StylesheetNode._(stylesheet, importer, canonicalUrl, _upstreamNodes(stylesheet, importer, canonicalUrl)); active.remove(canonicalUrl); _nodes[canonicalUrl] = node; @@ -145,9 +211,10 @@ class StylesheetGraph { /// /// A [StylesheetNode] is immutable except for its downstream nodes. When the /// stylesheet itself changes, a new node should be generated. -class _StylesheetNode { +class StylesheetNode { /// The parsed stylesheet. - final Stylesheet stylesheet; + Stylesheet get stylesheet => _stylesheet; + Stylesheet _stylesheet; /// The importer that was used to load this stylesheet. final Importer importer; @@ -159,28 +226,55 @@ class _StylesheetNode { /// stylesheets those imports refer to. /// /// This may have `null` values, which indicate failed imports. - final Map upstream; + Map get upstream => new UnmodifiableMapView(_upstream); + Map _upstream; /// The stylesheets that import [stylesheet]. - /// - /// This is automatically populated when new [_StylesheetNode]s are created - /// that list this as an upstream node. - final downstream = new Set<_StylesheetNode>(); + Set get downstream => new UnmodifiableSetView(_downstream); + final _downstream = new Set(); - _StylesheetNode(this.stylesheet, this.importer, this.canonicalUrl, - Map upstream) - : upstream = new Map.unmodifiable(upstream) { + StylesheetNode._( + this._stylesheet, this.importer, this.canonicalUrl, this._upstream) { for (var node in upstream.values) { - if (node != null) node.downstream.add(this); + if (node != null) node._downstream.add(this); } } - /// Removes [this] as a downstream node from all the upstream nodes that it - /// imports. - void remove() { - for (var node in upstream.values) { - var wasRemoved = node.downstream.remove(this); + /// Sets [newUpstream] as the new value of [upstream] and adjusts upstream + /// nodes' [downstream] fields accordingly. + void _replaceUpstream(Map newUpstream) { + var oldUpstream = new Set.of(upstream.values)..remove(null); + var newUpstreamSet = new Set.of(newUpstream.values)..remove(null); + + for (var removed in oldUpstream.difference(newUpstreamSet)) { + var wasRemoved = removed._downstream.remove(this); assert(wasRemoved); } + + for (var added in newUpstreamSet.difference(oldUpstream)) { + var wasAdded = added._downstream.add(this); + assert(wasAdded); + } + + _upstream = newUpstream; + } + + /// Removes [this] as an upstream and downstream node from all the nodes that + /// import it and that it imports. + void _remove() { + for (var node in upstream.values) { + if (node == null) continue; + var wasRemoved = node._downstream.remove(this); + assert(wasRemoved); + } + + for (var node in downstream) { + for (var url in node._upstream.keys.toList()) { + if (node._upstream[url] == this) { + node._upstream[url] = null; + break; + } + } + } } }