Generator/Sources/NeedleFramework/Parsing/DependencyGraphParser.swift (74 lines of code) (raw):
//
// Copyright (c) 2018. Uber Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Concurrency
import Foundation
import SourceParsingFramework
/// The entry utility for the parsing phase. The parser deeply scans a
/// directory and parses the relevant Swift source files, and finally
/// outputs the dependency graph.
class DependencyGraphParser: AbstractDependencyGraphParser {
/// Parse all the Swift sources within the directories of given URLs,
/// excluding any file that contains a suffix specified in the given
/// exclusion list. Parsing sources concurrently using the given executor.
///
/// - parameter rootUrls: The URLs of the directories to scan from.
/// - parameter sourcesListFormatValue: The optional `String` value of
/// the format used by the sources list file. If `nil` and the the given
/// `rootUrl` is a file containing a list of Swift source paths, the
/// `SourcesListFileFormat.newline` format is used. If the given `rootUrl`
/// is not a file containing a list of Swift source paths, this value is
/// ignored.
/// - parameter exclusionSuffixes: The list of file name suffixes to
/// check from. If a filename's suffix matches any in the this list,
/// the file will not be parsed.
/// - parameter exclusionPaths: The list of path components to check.
/// If a file's URL path contains any elements in this list, the file
/// will not be parsed.
/// - parameter executor: The executor to use for concurrent processing
/// of files.
/// - parameter timeout: The timeout value, in seconds, to use for
/// waiting on parsing tasks.
/// - returns: The list of component data models and sorted import
/// statements.
/// - throws: `DependencyGraphParserError.timeout` if parsing a Swift
/// source timed out.
func parse(from rootUrls: [URL], withSourcesListFormat sourcesListFormatValue: String?, excludingFilesEndingWith exclusionSuffixes: [String] = [], excludingFilesWithPaths exclusionPaths: [String] = [], using executor: SequenceExecutor, withTimeout timeout: TimeInterval) throws -> (components: [Component], imports: [String]) {
// Collect data models for component and dependency declarations.
let dependencyNodeUrlHandles: [DependencyNodeUrlSequenceHandle] = try enqueueDeclarationParsingTasks(with: rootUrls, sourcesListFormatValue: sourcesListFormatValue, excludingFilesEndingWith: exclusionSuffixes, excludingFilesWithPaths: exclusionPaths, using: executor)
let (components, dependencies, imports) = try collectDeclarationDataModels(with: dependencyNodeUrlHandles, waitUpTo: timeout)
// Collect data models for component extensions.
let (componentExtensions, extensionImports) = try parseAndCollectComponentExtensionDataModels(with: rootUrls, sourcesListFormatValue: sourcesListFormatValue, excludingFilesEndingWith: exclusionSuffixes, excludingFilesWithPaths: exclusionPaths, parsedComponents: components, using: executor, with: timeout)
let allImports = imports.union(extensionImports)
// Collect source contents that contain component instantiations for validation.
let initsSourceUrlContents = try sourceUrlContentsContainComponentInstantiations(with: rootUrls, sourcesListFormatValue: sourcesListFormatValue, excludingFilesEndingWith: exclusionSuffixes, excludingFilesWithPaths: exclusionPaths, using: executor, with: timeout)
// Process all the data models.
return try process(components, with: componentExtensions, dependencies, allImports, validate: initsSourceUrlContents, using: executor, with: timeout)
}
// MARK: - Declaration Parsing
private func enqueueDeclarationParsingTasks(with rootUrls: [URL], sourcesListFormatValue: String?, excludingFilesEndingWith exclusionSuffixes: [String], excludingFilesWithPaths exclusionPaths: [String], using executor: SequenceExecutor) throws -> [DependencyNodeUrlSequenceHandle] {
return try executeAndCollectTaskHandles(with: rootUrls, sourcesListFormatValue: sourcesListFormatValue) { (fileUrl: URL) -> SequenceExecutionHandle<DependencyGraphNode> in
let task = DeclarationsFilterTask(url: fileUrl, exclusionSuffixes: exclusionSuffixes, exclusionPaths: exclusionPaths)
return executor.executeSequence(from: task, with: declarationNextExecution(after:with:))
}
}
private func declarationNextExecution(after currentTask: Task, with currentResult: Any) -> SequenceExecution<DependencyGraphNode> {
if currentTask is DeclarationsFilterTask, let filterResult = currentResult as? FilterResult {
switch filterResult {
case .shouldProcess(let url, let content):
return .continueSequence(ASTProducerTask(sourceUrl: url, sourceContent: content))
case .skip:
return .endOfSequence(DependencyGraphNode(components: [], dependencies: [], imports: []))
}
} else if currentTask is ASTProducerTask, let ast = currentResult as? AST {
return .continueSequence(DeclarationsParserTask(ast: ast))
} else if currentTask is DeclarationsParserTask, let node = currentResult as? DependencyGraphNode {
return .endOfSequence(node)
} else {
error("Unhandled task \(currentTask) with result \(currentResult)")
}
}
private func collectDeclarationDataModels(with urlHandles: [DependencyNodeUrlSequenceHandle], waitUpTo timeout: TimeInterval) throws -> ([ASTComponent], [Dependency], Set<String>) {
var components = [ASTComponent]()
var dependencies = [Dependency]()
var imports = Set<String>()
for urlHandle in urlHandles {
do {
let node = try urlHandle.handle.await(withTimeout: timeout)
components.append(contentsOf: node.components)
dependencies.append(contentsOf: node.dependencies)
for statement in node.imports {
imports.insert(statement)
}
} catch SequenceExecutionError.awaitTimeout(let taskId) {
throw DependencyGraphParserError.timeout(urlHandle.fileUrl.absoluteString, taskId)
} catch {
throw error
}
}
return (components, dependencies, imports)
}
// MARK: - Processing
private func process(_ components: [ASTComponent], with componentExtensions: [ASTComponentExtension], _ dependencies: [Dependency], _ imports: Set<String>, validate initsSourceUrlContents: [UrlFileContent], using executor: SequenceExecutor, with timeout: TimeInterval) throws -> ([Component], [String]) {
let processors: [Processor] = [
DuplicateValidator(components: components, dependencies: dependencies),
ComponentConsolidator(components: components, componentExtensions: componentExtensions),
ParentLinker(components: components),
DependencyLinker(components: components, dependencies: dependencies),
AncestorCycleValidator(components: components),
ComponentInstantiationValidator(components: components, urlFileContents: initsSourceUrlContents, executor: executor, timeout: timeout)
]
for processor in processors {
try processor.process()
}
let valueTypeComponents = components.map { (astComponent: ASTComponent) -> Component in
astComponent.valueType
}
let sortedImports = imports.sorted()
return (valueTypeComponents, sortedImports)
}
}
private typealias DependencyNodeUrlSequenceHandle = (handle: SequenceExecutionHandle<DependencyGraphNode>, fileUrl: URL)