blob: 0a7ca257cf0382f776a701e3bf12dc6ceb663fe2 [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:collection';
import 'package:analyzer/context/context_root.dart' as old;
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/context_locator.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart'
show PhysicalResourceProvider;
import 'package:analyzer/src/context/builder.dart'
show ContextBuilder, ContextBuilderOptions;
import 'package:analyzer/src/dart/analysis/driver.dart'
show AnalysisDriver, AnalysisDriverScheduler;
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
import 'package:analyzer/src/dart/analysis/file_state.dart'
show FileContentOverlay;
import 'package:analyzer/src/dart/sdk/sdk.dart' show FolderBasedDartSdk;
import 'package:analyzer/src/generated/sdk.dart' show DartSdkManager;
import 'package:analyzer/src/generated/source.dart' show ContentCache;
import 'package:front_end/src/base/performance_logger.dart' show PerformanceLog;
import 'package:front_end/src/byte_store/byte_store.dart' show MemoryByteStore;
import 'package:meta/meta.dart';
/**
* An implementation of a context locator.
*/
class ContextLocatorImpl implements ContextLocator {
/**
* The name of the analysis options file.
*/
static const String ANALYSIS_OPTIONS_NAME = 'analysis_options.yaml';
/**
* The old name of the analysis options file.
*/
static const String OLD_ANALYSIS_OPTIONS_NAME = '.analysis_options';
/**
* The name of the packages folder.
*/
static const String PACKAGES_DIR_NAME = 'packages';
/**
* The name of the packages file.
*/
static const String PACKAGES_FILE_NAME = '.packages';
/**
* The resource provider used to access the file system.
*/
final ResourceProvider resourceProvider;
/**
* Initialize a newly created context locator. If a [resourceProvider] is
* supplied, it will be used to access the file system. Otherwise the default
* resource provider will be used.
*/
ContextLocatorImpl({ResourceProvider resourceProvider})
: this.resourceProvider =
resourceProvider ?? PhysicalResourceProvider.INSTANCE;
/**
* Return the path to the default location of the SDK.
*/
String get _defaultSdkPath =>
FolderBasedDartSdk.defaultSdkDirectory(resourceProvider).path;
@override
List<AnalysisContext> locateContexts(
{@required List<String> includedPaths,
List<String> excludedPaths: null,
String packagesFile: null,
String sdkPath: null}) {
if (includedPaths == null || includedPaths.isEmpty) {
throw new ArgumentError('There must be at least one included path');
}
List<AnalysisContext> contextList = <AnalysisContext>[];
List<ContextRoot> roots =
locateRoots(includedPaths, excludedPaths: excludedPaths);
PerformanceLog performanceLog = new PerformanceLog(new StringBuffer());
AnalysisDriverScheduler scheduler =
new AnalysisDriverScheduler(performanceLog);
DartSdkManager sdkManager =
new DartSdkManager(sdkPath ?? _defaultSdkPath, true);
scheduler.start();
ContextBuilderOptions options = new ContextBuilderOptions();
ContextBuilder builder = new ContextBuilder(
resourceProvider, sdkManager, new ContentCache(),
options: options);
if (packagesFile != null) {
options.defaultPackageFilePath = packagesFile;
}
builder.analysisDriverScheduler = scheduler;
builder.byteStore = new MemoryByteStore();
builder.fileContentOverlay = new FileContentOverlay();
builder.performanceLog = performanceLog;
for (ContextRoot root in roots) {
old.ContextRoot contextRoot =
new old.ContextRoot(root.root.path, root.excludedPaths);
AnalysisDriver driver = builder.buildDriver(contextRoot);
DriverBasedAnalysisContext context =
new DriverBasedAnalysisContext(resourceProvider, driver);
context.includedPaths = root.includedPaths;
context.excludedPaths = root.excludedPaths;
contextList.add(context);
}
return contextList;
}
/**
* Return a list of the context roots that should be used to analyze the files
* that are included by the list of [includedPaths] and not excluded by the
* list of [excludedPaths].
*/
@visibleForTesting
List<ContextRoot> locateRoots(List<String> includedPaths,
{List<String> excludedPaths}) {
//
// Compute the list of folders and files that are to be included.
//
List<Folder> includedFolders = <Folder>[];
List<File> includedFiles = <File>[];
_resourcesFromPaths(includedPaths, includedFolders, includedFiles);
//
// Compute the list of folders and files that are to be excluded.
//
List<Folder> excludedFolders = <Folder>[];
List<File> excludedFiles = <File>[];
_resourcesFromPaths(
excludedPaths ?? const <String>[], excludedFolders, excludedFiles);
//
// Use the excluded folders and files to filter the included folders and
// files.
//
includedFolders = includedFolders
.where((Folder includedFolder) =>
!_containedInAny(excludedFolders, includedFolder) &&
!_containedInAny(includedFolders, includedFolder))
.toList();
includedFiles = includedFiles
.where((File includedFile) =>
!_containedInAny(excludedFolders, includedFile) &&
!excludedFiles.contains(includedFile) &&
!_containedInAny(includedFolders, includedFile))
.toList();
//
// We now have a list of all of the files and folders that need to be
// analyzed. For each, walk the directory structure and figure out where to
// create context roots.
//
List<ContextRoot> roots = <ContextRoot>[];
for (Folder folder in includedFolders) {
_createContextRoots(roots, folder, excludedFolders, null);
}
for (File file in includedFiles) {
Folder parent = file.parent;
ContextRoot root = new ContextRoot(file);
root.packagesFile = _findPackagesFile(parent);
root.optionsFile = _findOptionsFile(parent);
root.included.add(file);
roots.add(root);
}
return roots;
}
/**
* Return `true` if the given [resource] is contained in one or more of the
* given [folders].
*/
bool _containedInAny(Iterable<Folder> folders, Resource resource) =>
folders.any((Folder folder) => folder.contains(resource.path));
void _createContextRoots(List<ContextRoot> roots, Folder folder,
List<Folder> excludedFolders, ContextRoot containingRoot) {
//
// Create a context root for the given [folder] is appropriate.
//
if (containingRoot == null) {
ContextRoot root = new ContextRoot(folder);
root.packagesFile = _findPackagesFile(folder);
root.optionsFile = _findOptionsFile(folder);
root.included.add(folder);
roots.add(root);
containingRoot = root;
} else {
File packagesFile = _getPackagesFile(folder);
File optionsFile = _getOptionsFile(folder);
if (packagesFile != null || optionsFile != null) {
ContextRoot root = new ContextRoot(folder);
root.packagesFile = packagesFile ?? containingRoot.packagesFile;
root.optionsFile = optionsFile ?? containingRoot.optionsFile;
root.included.add(folder);
containingRoot.excluded.add(folder);
roots.add(root);
containingRoot = root;
}
}
//
// Check each of the subdirectories to see whether a context root needs to
// be added for it.
//
try {
for (Resource child in folder.getChildren()) {
if (child is Folder) {
if (excludedFolders.contains(folder) ||
folder.shortName.startsWith('.') ||
folder.shortName == PACKAGES_DIR_NAME) {
containingRoot.excluded.add(folder);
} else {
_createContextRoots(roots, child, excludedFolders, containingRoot);
}
}
}
} on FileSystemException {
// The directory either doesn't exist or cannot be read. Either way, there
// are no subdirectories that need to be added.
}
}
/**
* Return the analysis options file to be used to analyze files in the given
* [folder], or `null` if there is no analysis options file in the given
* folder or any parent folder.
*/
File _findOptionsFile(Folder folder) {
while (folder != null) {
File packagesFile = _getOptionsFile(folder);
if (packagesFile != null) {
return packagesFile;
}
folder = folder.parent;
}
return null;
}
/**
* Return the packages file to be used to analyze files in the given [folder],
* or `null` if there is no packages file in the given folder or any parent
* folder.
*/
File _findPackagesFile(Folder folder) {
while (folder != null) {
File packagesFile = _getPackagesFile(folder);
if (packagesFile != null) {
return packagesFile;
}
folder = folder.parent;
}
return null;
}
/**
* If the given [directory] contains a file with the given [name], then return
* the file. Otherwise, return `null`.
*/
File _getFile(Folder directory, String name) {
Resource resource = directory.getChild(name);
if (resource is File && resource.exists) {
return resource;
}
return null;
}
/**
* Return the analysis options file in the given [folder], or `null` if the
* folder does not contain an analysis options file.
*/
File _getOptionsFile(Folder folder) =>
_getFile(folder, ANALYSIS_OPTIONS_NAME) ??
_getFile(folder, OLD_ANALYSIS_OPTIONS_NAME);
/**
* Return the packages file in the given [folder], or `null` if the folder
* does not contain a packages file.
*/
File _getPackagesFile(Folder folder) => _getFile(folder, PACKAGES_FILE_NAME);
/**
* Add to the given lists of [folders] and [files] all of the resources in the
* given list of [paths] that exist and are not contained within one of the
* folders.
*/
void _resourcesFromPaths(
List<String> paths, List<Folder> folders, List<File> files) {
for (String path in _uniqueSortedPaths(paths)) {
Resource resource = resourceProvider.getResource(path);
if (resource.exists && !_containedInAny(folders, resource)) {
if (resource is Folder) {
folders.add(resource);
} else if (resource is File) {
files.add(resource);
} else {
// Internal error: unhandled kind of resource.
}
}
}
}
/**
* Return a list of paths that contains all of the unique elements from the
* given list of [paths], sorted such that shorter paths are first.
*/
List<String> _uniqueSortedPaths(List<String> paths) {
Set<String> uniquePaths = new HashSet<String>.from(paths);
List<String> sortedPaths = uniquePaths.toList();
sortedPaths.sort((a, b) => a.length - b.length);
return sortedPaths;
}
}
@visibleForTesting
class ContextRoot {
final Resource root;
final List<Resource> included = <Resource>[];
final List<Resource> excluded = <Resource>[];
File packagesFile;
File optionsFile;
ContextRoot(this.root);
List<String> get excludedPaths =>
excluded.map((Resource folder) => folder.path).toList();
@override
int get hashCode => root.path.hashCode;
List<String> get includedPaths =>
included.map((Resource folder) => folder.path).toList();
@override
bool operator ==(Object other) {
return other is ContextRoot && root.path == other.root.path;
}
}