4ca5ac929a
On Windows, the MemoryResourceProvider.convertPath method prefixes the path with the C: drive, but the context associated with the MemoryResourceProvider has a drive letter based upon the underlying current directory. Most of the time, these two drive letters are the same, but sometimes, such as on the Windows bot, the current working directory is on drive E:, rather than drive C:. This causes package Uris to be mapped to drive E: rather than the expected drive C: This CL changes the MemoryResourceProvider constructor to set the current working directory to C:\ when running on Windows so that it aligns with MemoryResourceProvider.convertPath. R=brianwilkerson@google.com Review-Url: https://codereview.chromium.org/2714373002 .
590 lines
16 KiB
Dart
590 lines
16 KiB
Dart
// 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.
|
|
|
|
library analyzer.file_system.memory_file_system;
|
|
|
|
import 'dart:async';
|
|
import 'dart:collection';
|
|
import 'dart:convert';
|
|
import 'dart:core';
|
|
|
|
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:analyzer/src/util/absolute_path.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;
|
|
|
|
@override
|
|
final AbsolutePathContext absolutePathContext;
|
|
|
|
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),
|
|
absolutePathContext =
|
|
new AbsolutePathContext(context.style == pathos.Style.windows);
|
|
|
|
@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) => new _MemoryFile(this, path);
|
|
|
|
@override
|
|
Folder getFolder(String path) {
|
|
path = pathContext.normalize(path);
|
|
if (!pathContext.isAbsolute(path)) {
|
|
throw new ArgumentError("Path must be absolute : $path");
|
|
}
|
|
return new _MemoryFolder(this, path);
|
|
}
|
|
|
|
@override
|
|
Future<List<int>> getModificationTimes(List<Source> sources) async {
|
|
return sources.map((source) {
|
|
String path = source.fullName;
|
|
return _pathToTimestamp[path] ?? -1;
|
|
}).toList();
|
|
}
|
|
|
|
@override
|
|
Resource getResource(String path) {
|
|
path = pathContext.normalize(path);
|
|
Resource resource = _pathToResource[path];
|
|
if (resource == null) {
|
|
resource = new _MemoryFile(this, path);
|
|
}
|
|
return resource;
|
|
}
|
|
|
|
@override
|
|
Folder getStateLocation(String pluginId) {
|
|
return newFolder('/user/home/$pluginId');
|
|
}
|
|
|
|
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) {
|
|
path = pathContext.normalize(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]) {
|
|
path = pathContext.normalize(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]) {
|
|
path = pathContext.normalize(path);
|
|
_MemoryFile file = _newFile(path);
|
|
_pathToBytes[path] = bytes;
|
|
_pathToTimestamp[path] = stamp ?? nextStamp++;
|
|
_notifyWatchers(path, ChangeType.ADD);
|
|
return file;
|
|
}
|
|
|
|
Folder newFolder(String path) {
|
|
path = pathContext.normalize(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);
|
|
}
|
|
}
|
|
|
|
_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;
|
|
}
|
|
|
|
File updateFile(String path, String content, [int stamp]) {
|
|
path = pathContext.normalize(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) {
|
|
_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) {
|
|
_MemoryResource resource = _pathToResource[path];
|
|
if (resource is! _MemoryFolder) {
|
|
throw new ArgumentError(
|
|
'Folder expected at "$path" but ${resource.runtimeType} found');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|