blob: e17911562984e36b68358a12725c5937f9b93aa9 [file] [log] [blame]
// Copyright (c) 2014, 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:async';
import 'dart:collection';
import 'dart:convert';
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';
/**
* An in-memory implementation of [ResourceProvider].
* Use `/` as a path separator.
*/
class MemoryResourceProvider implements ResourceProvider {
final Map<String, _MemoryResource> _pathToResource =
new HashMap<String, _MemoryResource>();
final Map<String, List<int>> _pathToBytes = new HashMap<String, List<int>>();
final Map<String, int> _pathToTimestamp = new HashMap<String, int>();
final Map<String, List<StreamController<WatchEvent>>> _pathToWatchers =
new HashMap<String, List<StreamController<WatchEvent>>>();
int nextStamp = 0;
final pathos.Context _pathContext;
MemoryResourceProvider(
{pathos.Context context, @deprecated bool isWindows: false})
: _pathContext = (context ??= pathos.style == pathos.Style.windows
// On Windows, ensure that the current drive matches
// the drive inserted by MemoryResourceProvider.convertPath
// so that packages are mapped to the correct drive
? new pathos.Context(current: 'C:\\')
: pathos.context);
@override
pathos.Context get pathContext => _pathContext;
/**
* Convert the given posix [path] to conform to this provider's path context.
*
* This is a utility method for testing; paths passed in to other methods in
* this class are never converted automatically.
*/
String convertPath(String path) {
if (pathContext.style == pathos.windows.style) {
if (path.startsWith(pathos.posix.separator)) {
path = r'C:' + path;
}
path = path.replaceAll(pathos.posix.separator, pathos.windows.separator);
}
return path;
}
/**
* Delete the file with the given path.
*/
void deleteFile(String path) {
_checkFileAtPath(path);
_pathToResource.remove(path);
_pathToBytes.remove(path);
_pathToTimestamp.remove(path);
_notifyWatchers(path, ChangeType.REMOVE);
}
/**
* Delete the folder with the given path
* and recursively delete nested files and folders.
*/
void deleteFolder(String path) {
_checkFolderAtPath(path);
_MemoryFolder folder = _pathToResource[path];
for (Resource child in folder.getChildren()) {
if (child is File) {
deleteFile(child.path);
} else if (child is Folder) {
deleteFolder(child.path);
} else {
throw 'failed to delete resource: $child';
}
}
_pathToResource.remove(path);
_pathToBytes.remove(path);
_pathToTimestamp.remove(path);
_notifyWatchers(path, ChangeType.REMOVE);
}
@override
File getFile(String path) {
_ensureAbsoluteAndNormalized(path);
return new _MemoryFile(this, path);
}
@override
Folder getFolder(String path) {
_ensureAbsoluteAndNormalized(path);
return new _MemoryFolder(this, path);
}
@override
Future<List<int>> getModificationTimes(List<Source> sources) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
return sources.map((source) {
String path = source.fullName;
return _pathToTimestamp[path] ?? -1;
}).toList();
}
@override
Resource getResource(String path) {
_ensureAbsoluteAndNormalized(path);
Resource resource = _pathToResource[path];
if (resource == null) {
resource = new _MemoryFile(this, path);
}
return resource;
}
@override
Folder getStateLocation(String pluginId) {
var path = convertPath('/user/home/$pluginId');
return newFolder(path);
}
void modifyFile(String path, String content) {
_checkFileAtPath(path);
_pathToBytes[path] = utf8.encode(content);
_pathToTimestamp[path] = nextStamp++;
_notifyWatchers(path, ChangeType.MODIFY);
}
/**
* Create a resource representing a dummy link (that is, a File object which
* appears in its parent directory, but whose `exists` property is false)
*/
File newDummyLink(String path) {
_ensureAbsoluteAndNormalized(path);
newFolder(pathContext.dirname(path));
_MemoryDummyLink link = new _MemoryDummyLink(this, path);
_pathToResource[path] = link;
_pathToTimestamp[path] = nextStamp++;
_notifyWatchers(path, ChangeType.ADD);
return link;
}
File newFile(String path, String content, [int stamp]) {
_ensureAbsoluteAndNormalized(path);
_MemoryFile file = _newFile(path);
_pathToBytes[path] = utf8.encode(content);
_pathToTimestamp[path] = stamp ?? nextStamp++;
_notifyWatchers(path, ChangeType.ADD);
return file;
}
File newFileWithBytes(String path, List<int> bytes, [int stamp]) {
_ensureAbsoluteAndNormalized(path);
_MemoryFile file = _newFile(path);
_pathToBytes[path] = bytes;
_pathToTimestamp[path] = stamp ?? nextStamp++;
_notifyWatchers(path, ChangeType.ADD);
return file;
}
Folder newFolder(String path) {
_ensureAbsoluteAndNormalized(path);
if (!pathContext.isAbsolute(path)) {
throw new ArgumentError("Path must be absolute : $path");
}
_MemoryResource resource = _pathToResource[path];
if (resource == null) {
String parentPath = pathContext.dirname(path);
if (parentPath != path) {
newFolder(parentPath);
}
_MemoryFolder folder = new _MemoryFolder(this, path);
_pathToResource[path] = folder;
_pathToTimestamp[path] = nextStamp++;
_notifyWatchers(path, ChangeType.ADD);
return folder;
} else if (resource is _MemoryFolder) {
_notifyWatchers(path, ChangeType.ADD);
return resource;
} else {
String message =
'Folder expected at ' "'$path'" 'but ${resource.runtimeType} found';
throw new ArgumentError(message);
}
}
File updateFile(String path, String content, [int stamp]) {
_ensureAbsoluteAndNormalized(path);
newFolder(pathContext.dirname(path));
_MemoryFile file = new _MemoryFile(this, path);
_pathToResource[path] = file;
_pathToBytes[path] = utf8.encode(content);
_pathToTimestamp[path] = stamp ?? nextStamp++;
_notifyWatchers(path, ChangeType.MODIFY);
return file;
}
/**
* Write a representation of the file system on the given [sink].
*/
void writeOn(StringSink sink) {
List<String> paths = _pathToResource.keys.toList();
paths.sort();
paths.forEach(sink.writeln);
}
void _checkFileAtPath(String path) {
// TODO(brianwilkerson) Consider throwing a FileSystemException rather than
// an ArgumentError.
_MemoryResource resource = _pathToResource[path];
if (resource is! _MemoryFile) {
if (resource == null) {
throw new ArgumentError('File expected at "$path" but does not exist');
}
throw new ArgumentError(
'File expected at "$path" but ${resource.runtimeType} found');
}
}
void _checkFolderAtPath(String path) {
// TODO(brianwilkerson) Consider throwing a FileSystemException rather than
// an ArgumentError.
_MemoryResource resource = _pathToResource[path];
if (resource is! _MemoryFolder) {
throw new ArgumentError(
'Folder expected at "$path" but ${resource.runtimeType} found');
}
}
/**
* The file system abstraction supports only absolute and normalized paths.
* This method is used to validate any input paths to prevent errors later.
*/
void _ensureAbsoluteAndNormalized(String path) {
if (!pathContext.isAbsolute(path)) {
throw new ArgumentError("Path must be absolute : $path");
}
if (pathContext.normalize(path) != path) {
throw new ArgumentError("Path must be normalized : $path");
}
}
/**
* Create a new [_MemoryFile] without any content.
*/
_MemoryFile _newFile(String path) {
String folderPath = pathContext.dirname(path);
_MemoryResource folder = _pathToResource[folderPath];
if (folder == null) {
newFolder(folderPath);
} else if (folder is! Folder) {
throw new ArgumentError('Cannot create file ($path) as child of file');
}
_MemoryFile file = new _MemoryFile(this, path);
_pathToResource[path] = file;
return file;
}
void _notifyWatchers(String path, ChangeType changeType) {
_pathToWatchers.forEach((String watcherPath,
List<StreamController<WatchEvent>> streamControllers) {
if (watcherPath == path || pathContext.isWithin(watcherPath, path)) {
for (StreamController<WatchEvent> streamController
in streamControllers) {
streamController.add(new WatchEvent(changeType, path));
}
}
});
}
_MemoryFile _renameFileSync(_MemoryFile file, String newPath) {
String path = file.path;
if (newPath == path) {
return file;
}
_MemoryResource existingNewResource = _pathToResource[newPath];
if (existingNewResource is _MemoryFolder) {
throw new FileSystemException(
path, 'Could not be renamed: $newPath is a folder.');
}
_MemoryFile newFile = _newFile(newPath);
_pathToResource.remove(path);
_pathToBytes[newPath] = _pathToBytes.remove(path);
_pathToTimestamp[newPath] = _pathToTimestamp.remove(path);
if (existingNewResource != null) {
_notifyWatchers(newPath, ChangeType.REMOVE);
}
_notifyWatchers(path, ChangeType.REMOVE);
_notifyWatchers(newPath, ChangeType.ADD);
return newFile;
}
void _setFileContent(_MemoryFile file, List<int> bytes) {
String path = file.path;
_pathToResource[path] = file;
_pathToBytes[path] = bytes;
_pathToTimestamp[path] = nextStamp++;
_notifyWatchers(path, ChangeType.MODIFY);
}
}
/**
* An in-memory implementation of [File] which acts like a symbolic link to a
* non-existent file.
*/
class _MemoryDummyLink extends _MemoryResource implements File {
_MemoryDummyLink(MemoryResourceProvider provider, String path)
: super(provider, path);
@override
Stream<WatchEvent> get changes {
throw new FileSystemException(path, "File does not exist");
}
@override
bool get exists => false;
@override
int get lengthSync {
throw new FileSystemException(path, 'File could not be read');
}
@override
int get modificationStamp {
int stamp = _provider._pathToTimestamp[path];
if (stamp == null) {
throw new FileSystemException(path, "File does not exist");
}
return stamp;
}
@override
File copyTo(Folder parentFolder) {
throw new FileSystemException(path, 'File could not be copied');
}
@override
Source createSource([Uri uri]) {
throw new FileSystemException(path, 'File could not be read');
}
@override
void delete() {
throw new FileSystemException(path, 'File could not be deleted');
}
@override
bool isOrContains(String path) {
return path == this.path;
}
@override
List<int> readAsBytesSync() {
throw new FileSystemException(path, 'File could not be read');
}
@override
String readAsStringSync() {
throw new FileSystemException(path, 'File could not be read');
}
@override
File renameSync(String newPath) {
throw new FileSystemException(path, 'File could not be renamed');
}
@override
File resolveSymbolicLinksSync() {
return throw new FileSystemException(path, "File does not exist");
}
@override
void writeAsBytesSync(List<int> bytes) {
throw new FileSystemException(path, 'File could not be written');
}
@override
void writeAsStringSync(String content) {
throw new FileSystemException(path, 'File could not be written');
}
}
/**
* An in-memory implementation of [File].
*/
class _MemoryFile extends _MemoryResource implements File {
_MemoryFile(MemoryResourceProvider provider, String path)
: super(provider, path);
@override
bool get exists => _provider._pathToResource[path] is _MemoryFile;
@override
int get lengthSync {
return readAsBytesSync().length;
}
@override
int get modificationStamp {
int stamp = _provider._pathToTimestamp[path];
if (stamp == null) {
throw new FileSystemException(path, 'File "$path" does not exist.');
}
return stamp;
}
@override
File copyTo(Folder parentFolder) {
parentFolder.create();
File destination = parentFolder.getChildAssumingFile(shortName);
destination.writeAsBytesSync(readAsBytesSync());
return destination;
}
@override
Source createSource([Uri uri]) {
uri ??= _provider.pathContext.toUri(path);
return new FileSource(this, uri);
}
@override
void delete() {
_provider.deleteFile(path);
}
@override
bool isOrContains(String path) {
return path == this.path;
}
@override
List<int> readAsBytesSync() {
List<int> content = _provider._pathToBytes[path];
if (content == null) {
throw new FileSystemException(path, 'File "$path" does not exist.');
}
return content;
}
@override
String readAsStringSync() {
List<int> content = _provider._pathToBytes[path];
if (content == null) {
throw new FileSystemException(path, 'File "$path" does not exist.');
}
return utf8.decode(content);
}
@override
File renameSync(String newPath) {
return _provider._renameFileSync(this, newPath);
}
@override
File resolveSymbolicLinksSync() => this;
@override
void writeAsBytesSync(List<int> bytes) {
_provider._setFileContent(this, bytes);
}
@override
void writeAsStringSync(String content) {
_provider._setFileContent(this, utf8.encode(content));
}
}
/**
* An in-memory implementation of [Folder].
*/
class _MemoryFolder extends _MemoryResource implements Folder {
_MemoryFolder(MemoryResourceProvider provider, String path)
: super(provider, path);
@override
bool get exists => _provider._pathToResource[path] is _MemoryFolder;
@override
String canonicalizePath(String relPath) {
relPath = _provider.pathContext.normalize(relPath);
String childPath = _provider.pathContext.join(path, relPath);
childPath = _provider.pathContext.normalize(childPath);
return childPath;
}
@override
bool contains(String path) {
return _provider.pathContext.isWithin(this.path, 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() {
_provider.newFolder(path);
}
@override
void delete() {
_provider.deleteFolder(path);
}
@override
Resource getChild(String relPath) {
String childPath = canonicalizePath(relPath);
_MemoryResource resource = _provider._pathToResource[childPath];
if (resource == null) {
resource = new _MemoryFile(_provider, childPath);
}
return resource;
}
@override
_MemoryFile getChildAssumingFile(String relPath) {
String childPath = canonicalizePath(relPath);
_MemoryResource resource = _provider._pathToResource[childPath];
if (resource is _MemoryFile) {
return resource;
}
return new _MemoryFile(_provider, childPath);
}
@override
_MemoryFolder getChildAssumingFolder(String relPath) {
String childPath = canonicalizePath(relPath);
_MemoryResource resource = _provider._pathToResource[childPath];
if (resource is _MemoryFolder) {
return resource;
}
return new _MemoryFolder(_provider, childPath);
}
@override
List<Resource> getChildren() {
if (!exists) {
throw new FileSystemException(path, 'Folder does not exist.');
}
List<Resource> children = <Resource>[];
_provider._pathToResource.forEach((resourcePath, resource) {
if (_provider.pathContext.dirname(resourcePath) == path) {
children.add(resource);
}
});
return children;
}
@override
bool isOrContains(String path) {
if (path == this.path) {
return true;
}
return contains(path);
}
@override
Folder resolveSymbolicLinksSync() => this;
@override
Uri toUri() => _provider.pathContext.toUri(path + '/');
}
/**
* An in-memory implementation of [Resource].
*/
abstract class _MemoryResource implements Resource {
final MemoryResourceProvider _provider;
@override
final String path;
_MemoryResource(this._provider, this.path);
Stream<WatchEvent> get changes {
StreamController<WatchEvent> streamController =
new StreamController<WatchEvent>();
if (!_provider._pathToWatchers.containsKey(path)) {
_provider._pathToWatchers[path] = <StreamController<WatchEvent>>[];
}
_provider._pathToWatchers[path].add(streamController);
streamController.done.then((_) {
_provider._pathToWatchers[path].remove(streamController);
if (_provider._pathToWatchers[path].isEmpty) {
_provider._pathToWatchers.remove(path);
}
});
return streamController.stream;
}
@override
get hashCode => path.hashCode;
@override
Folder get parent {
String parentPath = _provider.pathContext.dirname(path);
if (parentPath == path) {
return null;
}
return _provider.getFolder(parentPath);
}
@override
String get shortName => _provider.pathContext.basename(path);
@override
bool operator ==(other) {
if (runtimeType != other.runtimeType) {
return false;
}
return path == other.path;
}
@override
String toString() => path;
@override
Uri toUri() => _provider.pathContext.toUri(path);
}