4366ff1afd
Change-Id: Ic4266f4ad1f46e405e22986e3660a3889ec7016c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/187060 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
416 lines
13 KiB
Dart
416 lines
13 KiB
Dart
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
import 'dart:convert';
|
|
import 'dart:core';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:analyzer/file_system/file_system.dart';
|
|
import 'package:analyzer/src/generated/source_io.dart';
|
|
import 'package:analyzer/src/source/source_resource.dart';
|
|
import 'package:path/path.dart' as pathos;
|
|
import 'package:watcher/watcher.dart';
|
|
|
|
/// A resource provider that allows clients to overlay the file system provided
|
|
/// by a base resource provider. These overlays allow both the contents and
|
|
/// modification stamps of files to be different than what the base resource
|
|
/// provider would report.
|
|
///
|
|
/// This provider does not report watch events when overlays are added, modified
|
|
/// or removed.
|
|
class OverlayResourceProvider implements ResourceProvider {
|
|
/// The underlying resource provider used to access files and folders that
|
|
/// do not have an overlay.
|
|
final ResourceProvider baseProvider;
|
|
|
|
/// A map from the paths of files for which there is an overlay to the
|
|
/// contents of the files.
|
|
final Map<String, String> _overlayContent = <String, String>{};
|
|
|
|
/// A map from the paths of files for which there is an overlay to the
|
|
/// modification stamps of the files.
|
|
final Map<String, int> _overlayModificationStamps = <String, int>{};
|
|
|
|
/// Initialize a newly created resource provider to represent an overlay on
|
|
/// the given [baseProvider].
|
|
OverlayResourceProvider(this.baseProvider);
|
|
|
|
@override
|
|
pathos.Context get pathContext => baseProvider.pathContext;
|
|
|
|
@override
|
|
File getFile(String path) => _OverlayFile(this, baseProvider.getFile(path));
|
|
|
|
@override
|
|
Folder getFolder(String path) =>
|
|
_OverlayFolder(this, baseProvider.getFolder(path));
|
|
|
|
@override
|
|
Future<List<int>> getModificationTimes(List<Source> sources) async {
|
|
return sources.map((source) {
|
|
String path = source.fullName;
|
|
return _overlayModificationStamps[path] ??
|
|
baseProvider.getFile(path).modificationStamp;
|
|
}).toList();
|
|
}
|
|
|
|
@override
|
|
Resource getResource(String path) {
|
|
if (hasOverlay(path)) {
|
|
return _OverlayResource._from(this, baseProvider.getFile(path));
|
|
} else if (_hasOverlayIn(path)) {
|
|
return _OverlayResource._from(this, baseProvider.getFolder(path));
|
|
}
|
|
return _OverlayResource._from(this, baseProvider.getResource(path));
|
|
}
|
|
|
|
@override
|
|
Folder? getStateLocation(String pluginId) {
|
|
var location = baseProvider.getStateLocation(pluginId);
|
|
return location != null ? _OverlayFolder(this, location) : null;
|
|
}
|
|
|
|
/// Return `true` if there is an overlay associated with the file at the given
|
|
/// [path].
|
|
bool hasOverlay(String path) => _overlayContent.containsKey(path);
|
|
|
|
/// Remove any overlay of the file at the given [path]. The state of the file
|
|
/// in the base resource provider will not be affected.
|
|
bool removeOverlay(String path) {
|
|
bool hadOverlay = _overlayContent.containsKey(path);
|
|
_overlayContent.remove(path);
|
|
_overlayModificationStamps.remove(path);
|
|
return hadOverlay;
|
|
}
|
|
|
|
/// Overlay the content of the file at the given [path]. The file will appear
|
|
/// to have the given [content] and [modificationStamp] even if the file is
|
|
/// modified in the base resource provider.
|
|
void setOverlay(String path,
|
|
{required String content, required int modificationStamp}) {
|
|
_overlayContent[path] = content;
|
|
_overlayModificationStamps[path] = modificationStamp;
|
|
}
|
|
|
|
/// Copy any overlay for the file at the [oldPath] to be an overlay for the
|
|
/// file with the [newPath].
|
|
void _copyOverlay(String oldPath, String newPath) {
|
|
if (hasOverlay(oldPath)) {
|
|
_overlayContent[newPath] = _overlayContent[oldPath]!;
|
|
_overlayModificationStamps[newPath] =
|
|
_overlayModificationStamps[oldPath]!;
|
|
}
|
|
}
|
|
|
|
/// Return the content of the overlay of the file at the given [path], or
|
|
/// `null` if there is no overlay for the specified file.
|
|
String? _getOverlayContent(String path) {
|
|
return _overlayContent[path];
|
|
}
|
|
|
|
/// Return the modification stamp of the overlay of the file at the given
|
|
/// [path], or `null` if there is no overlay for the specified file.
|
|
int? _getOverlayModificationStamp(String path) {
|
|
return _overlayModificationStamps[path];
|
|
}
|
|
|
|
/// Return `true` if there is an overlay associated with at least one file
|
|
/// contained inside the folder with the given [folderPath].
|
|
bool _hasOverlayIn(String folderPath) => _overlayContent.keys
|
|
.any((filePath) => pathContext.isWithin(folderPath, filePath));
|
|
|
|
/// Return the paths of all of the overlaid files that are children of the
|
|
/// given [folder], either directly or indirectly.
|
|
Iterable<String> _overlaysInFolder(String folderPath) => _overlayContent.keys
|
|
.where((filePath) => pathContext.isWithin(folderPath, filePath));
|
|
}
|
|
|
|
/// A file from an [OverlayResourceProvider].
|
|
class _OverlayFile extends _OverlayResource implements File {
|
|
/// Initialize a newly created file to have the given [provider] and to
|
|
/// correspond to the given [file] from the provider's base resource provider.
|
|
_OverlayFile(OverlayResourceProvider provider, File file)
|
|
: super(provider, file);
|
|
|
|
@override
|
|
Stream<WatchEvent> get changes => _file.changes;
|
|
|
|
@override
|
|
bool get exists => provider.hasOverlay(path) || _resource.exists;
|
|
|
|
@override
|
|
int get lengthSync {
|
|
String? content = provider._getOverlayContent(path);
|
|
if (content != null) {
|
|
return content.length;
|
|
}
|
|
return _file.lengthSync;
|
|
}
|
|
|
|
@override
|
|
int get modificationStamp {
|
|
int? stamp = provider._getOverlayModificationStamp(path);
|
|
if (stamp != null) {
|
|
return stamp;
|
|
}
|
|
return _file.modificationStamp;
|
|
}
|
|
|
|
/// Return the file from the base resource provider that corresponds to this
|
|
/// folder.
|
|
File get _file => _resource as File;
|
|
|
|
@override
|
|
File copyTo(Folder parentFolder) {
|
|
String newPath = provider.pathContext.join(parentFolder.path, shortName);
|
|
provider._copyOverlay(path, newPath);
|
|
if (_file.exists) {
|
|
if (parentFolder is _OverlayFolder) {
|
|
return _OverlayFile(provider, _file.copyTo(parentFolder._folder));
|
|
}
|
|
return _OverlayFile(provider, _file.copyTo(parentFolder));
|
|
} else {
|
|
return _OverlayFile(provider, provider.baseProvider.getFile(newPath));
|
|
}
|
|
}
|
|
|
|
@override
|
|
Source createSource([Uri? uri]) =>
|
|
FileSource(this, uri ?? provider.pathContext.toUri(path));
|
|
|
|
@override
|
|
void delete() {
|
|
bool hadOverlay = provider.removeOverlay(path);
|
|
if (_resource.exists) {
|
|
_resource.delete();
|
|
} else if (!hadOverlay) {
|
|
throw FileSystemException(path, 'does not exist');
|
|
}
|
|
}
|
|
|
|
@override
|
|
List<int> readAsBytesSync() {
|
|
String? content = provider._getOverlayContent(path);
|
|
if (content != null) {
|
|
return utf8.encode(content) as Uint8List;
|
|
}
|
|
return _file.readAsBytesSync();
|
|
}
|
|
|
|
@override
|
|
String readAsStringSync() {
|
|
String? content = provider._getOverlayContent(path);
|
|
if (content != null) {
|
|
return content;
|
|
}
|
|
return _file.readAsStringSync();
|
|
}
|
|
|
|
@override
|
|
File renameSync(String newPath) {
|
|
File newFile = _file.renameSync(newPath);
|
|
if (provider.hasOverlay(path)) {
|
|
provider.setOverlay(
|
|
newPath,
|
|
content: provider._getOverlayContent(path)!,
|
|
modificationStamp: provider._getOverlayModificationStamp(path)!,
|
|
);
|
|
provider.removeOverlay(path);
|
|
}
|
|
return _OverlayFile(provider, newFile);
|
|
}
|
|
|
|
@override
|
|
void writeAsBytesSync(List<int> bytes) {
|
|
writeAsStringSync(String.fromCharCodes(bytes));
|
|
}
|
|
|
|
@override
|
|
void writeAsStringSync(String content) {
|
|
if (provider.hasOverlay(path)) {
|
|
throw FileSystemException(path, 'Cannot write a file with an overlay');
|
|
}
|
|
_file.writeAsStringSync(content);
|
|
}
|
|
}
|
|
|
|
/// A folder from an [OverlayResourceProvider].
|
|
class _OverlayFolder extends _OverlayResource implements Folder {
|
|
/// Initialize a newly created folder to have the given [provider] and to
|
|
/// correspond to the given [folder] from the provider's base resource
|
|
/// provider.
|
|
_OverlayFolder(OverlayResourceProvider provider, Folder folder)
|
|
: super(provider, folder);
|
|
|
|
@override
|
|
Stream<WatchEvent> get changes => _folder.changes;
|
|
|
|
@override
|
|
bool get exists => provider._hasOverlayIn(path) || _resource.exists;
|
|
|
|
@override
|
|
bool get isRoot {
|
|
var parentPath = provider.pathContext.dirname(path);
|
|
return parentPath == path;
|
|
}
|
|
|
|
/// Return the folder from the base resource provider that corresponds to this
|
|
/// folder.
|
|
Folder get _folder => _resource as Folder;
|
|
|
|
@override
|
|
String canonicalizePath(String relPath) {
|
|
pathos.Context context = provider.pathContext;
|
|
relPath = context.normalize(relPath);
|
|
String childPath = context.join(path, relPath);
|
|
childPath = context.normalize(childPath);
|
|
return childPath;
|
|
}
|
|
|
|
@override
|
|
bool contains(String path) => _folder.contains(path);
|
|
|
|
@override
|
|
Folder copyTo(Folder parentFolder) {
|
|
Folder destination = parentFolder.getChildAssumingFolder(shortName);
|
|
destination.create();
|
|
for (Resource child in getChildren()) {
|
|
child.copyTo(destination);
|
|
}
|
|
return destination;
|
|
}
|
|
|
|
@override
|
|
void create() {
|
|
_folder.create();
|
|
}
|
|
|
|
@override
|
|
Resource getChild(String relPath) =>
|
|
_OverlayResource._from(provider, _folder.getChild(relPath));
|
|
|
|
@override
|
|
File getChildAssumingFile(String relPath) =>
|
|
_OverlayFile(provider, _folder.getChildAssumingFile(relPath));
|
|
|
|
@override
|
|
Folder getChildAssumingFolder(String relPath) =>
|
|
_OverlayFolder(provider, _folder.getChildAssumingFolder(relPath));
|
|
|
|
@override
|
|
List<Resource> getChildren() {
|
|
Map<String, Resource> children = {};
|
|
try {
|
|
for (final child in _folder.getChildren()) {
|
|
children[child.path] = _OverlayResource._from(provider, child);
|
|
}
|
|
} on FileSystemException {
|
|
// We don't want to throw if we're a folder that only exists in the
|
|
// overlay and not on disk.
|
|
}
|
|
|
|
for (String overlayPath in provider._overlaysInFolder(path)) {
|
|
pathos.Context context = provider.pathContext;
|
|
if (context.dirname(overlayPath) == path) {
|
|
children.putIfAbsent(overlayPath, () => provider.getFile(overlayPath));
|
|
} else {
|
|
String relativePath = context.relative(overlayPath, from: path);
|
|
String folderName = context.split(relativePath)[0];
|
|
String folderPath = context.join(path, folderName);
|
|
children.putIfAbsent(folderPath, () => provider.getFolder(folderPath));
|
|
}
|
|
}
|
|
return children.values.toList();
|
|
}
|
|
}
|
|
|
|
/// The base class for resources from an [OverlayResourceProvider].
|
|
abstract class _OverlayResource implements Resource {
|
|
@override
|
|
final OverlayResourceProvider provider;
|
|
|
|
/// The resource from the provider's base provider that corresponds to this
|
|
/// resource.
|
|
final Resource _resource;
|
|
|
|
/// Initialize a newly created instance of a resource to have the given
|
|
/// [provider] and to represent the [_resource] from the provider's base
|
|
/// resource provider.
|
|
_OverlayResource(this.provider, this._resource);
|
|
|
|
/// Return an instance of the subclass of this class corresponding to the
|
|
/// given [resource] that is associated with the given [provider].
|
|
factory _OverlayResource._from(
|
|
OverlayResourceProvider provider, Resource resource) {
|
|
if (resource is Folder) {
|
|
return _OverlayFolder(provider, resource);
|
|
} else if (resource is File) {
|
|
return _OverlayFile(provider, resource);
|
|
}
|
|
throw ArgumentError('Unknown resource type: ${resource.runtimeType}');
|
|
}
|
|
|
|
@override
|
|
int get hashCode => path.hashCode;
|
|
|
|
@Deprecated('Use parent2 instead')
|
|
@override
|
|
Folder? get parent {
|
|
Folder? parent = _resource.parent;
|
|
if (parent == null) {
|
|
return null;
|
|
}
|
|
return _OverlayFolder(provider, parent);
|
|
}
|
|
|
|
@override
|
|
Folder get parent2 {
|
|
var parent = _resource.parent2;
|
|
return _OverlayFolder(provider, parent);
|
|
}
|
|
|
|
@override
|
|
String get path => _resource.path;
|
|
|
|
@override
|
|
String get shortName => _resource.shortName;
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (runtimeType != other.runtimeType) {
|
|
return false;
|
|
}
|
|
return path == (other as _OverlayResource).path;
|
|
}
|
|
|
|
@override
|
|
void delete() {
|
|
_resource.delete();
|
|
}
|
|
|
|
@override
|
|
bool isOrContains(String path) {
|
|
return _resource.isOrContains(path);
|
|
}
|
|
|
|
@override
|
|
Resource resolveSymbolicLinksSync() {
|
|
try {
|
|
var resolved = _resource.resolveSymbolicLinksSync();
|
|
return _OverlayResource._from(provider, resolved);
|
|
} catch (_) {
|
|
if (provider.hasOverlay(path) || provider._hasOverlayIn(path)) {
|
|
return this;
|
|
}
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
@override
|
|
String toString() => path;
|
|
|
|
@override
|
|
Uri toUri() => _resource.toUri();
|
|
}
|