blob: 563954b42aa46e3585acca27b5944c204e3ea6c3 [file] [log] [blame]
// 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 to the overlay data.
final Map<String, _OverlayFileData> _overlays = {};
/// 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 _overlays[path]?.modificationStamp ??
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) => _overlays.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) {
return _overlays.remove(path) != null;
}
/// 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}) {
_overlays[path] = _OverlayFileData(content, 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) {
var data = _overlays[oldPath];
if (data != null) {
_overlays[newPath] = data;
}
}
/// 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 _overlays[path]?.content;
}
/// 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 _overlays[path]?.modificationStamp;
}
/// 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) => _overlays.keys
.any((filePath) => pathContext.isWithin(folderPath, filePath));
/// Move any overlay for the file at the [oldPath] to be an overlay for the
/// file with the [newPath].
void _moveOverlay(String oldPath, String newPath) {
var data = _overlays.remove(oldPath);
if (data != null) {
_overlays[newPath] = data;
}
}
/// 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) => _overlays.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);
provider._moveOverlay(path, newPath);
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);
}
}
/// Overlay data for a file.
class _OverlayFileData {
final String content;
final int modificationStamp;
_OverlayFileData(this.content, this.modificationStamp);
}
/// 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();
}