Generator/Sources/NeedleFramework/Parsing/Pluginized/PluginizedDependencyGraphParser.swift (118 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 PluginizedDependencyGraphParser: 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, pluginized 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 -> ([Component], [PluginizedComponent], [String], String, Set<String>) {
// Collect data models for component and dependency declarations.
let urlHandles: [DependencyNodeUrlSequenceHandle] = try enqueueDeclarationParsingTasks(with: rootUrls, sourcesListFormatValue: sourcesListFormatValue, excludingFilesEndingWith: exclusionSuffixes, excludingFilesWithPaths: exclusionPaths, using: executor)
let (pluginizedComponents, nonCoreComponents, pluginExtensions, regularComponents, dependencies, imports) = try collectDataModels(with: urlHandles, waitUpTo: timeout)
// Collect data models for component extensions.
let allComponents = commonComponentModel(of: pluginizedComponents, regularComponents: nonCoreComponents + regularComponents)
let (componentExtensions, extensionImports) = try parseAndCollectComponentExtensionDataModels(with: rootUrls, sourcesListFormatValue: sourcesListFormatValue, excludingFilesEndingWith: exclusionSuffixes, excludingFilesWithPaths: exclusionPaths, parsedComponents: allComponents, 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)
return try process(pluginizedComponents: pluginizedComponents, nonCoreComponents: nonCoreComponents, regularComponents: regularComponents, with: pluginExtensions, 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<PluginizedDependencyGraphNode> in
let task = PluginizedDeclarationsFilterTask(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<PluginizedDependencyGraphNode> {
if currentTask is PluginizedDeclarationsFilterTask, let filterResult = currentResult as? FilterResult {
switch filterResult {
case .shouldProcess(let url, let content):
return .continueSequence(ASTProducerTask(sourceUrl: url, sourceContent: content))
case .skip:
return .endOfSequence(PluginizedDependencyGraphNode(pluginizedComponents: [], nonCoreComponents: [], pluginExtensions: [], components: [], dependencies: [], imports: []))
}
} else if currentTask is ASTProducerTask, let ast = currentResult as? AST {
return .continueSequence(PluginizedDeclarationsParserTask(ast: ast))
} else if currentTask is PluginizedDeclarationsParserTask, let node = currentResult as? PluginizedDependencyGraphNode {
return .endOfSequence(node)
} else {
error("Unhandled task \(currentTask) with result \(currentResult)")
}
}
private func collectDataModels(with urlHandles: [DependencyNodeUrlSequenceHandle], waitUpTo timeout: TimeInterval) throws -> ([PluginizedASTComponent], [ASTComponent], [PluginExtension], [ASTComponent], [Dependency], Set<String>) {
var pluginizedComponents = [PluginizedASTComponent]()
var nonCoreComponents = [ASTComponent]()
var pluginExtensions = [PluginExtension]()
var components = [ASTComponent]()
var dependencies = [Dependency]()
var imports = Set<String>()
for urlHandle in urlHandles {
do {
let node = try urlHandle.handle.await(withTimeout: timeout)
pluginizedComponents.append(contentsOf: node.pluginizedComponents)
nonCoreComponents.append(contentsOf: node.nonCoreComponents)
pluginExtensions.append(contentsOf: node.pluginExtensions)
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 (pluginizedComponents, nonCoreComponents, pluginExtensions, components, dependencies, imports)
}
// MARK: - Processing
private func process(pluginizedComponents: [PluginizedASTComponent], nonCoreComponents: [ASTComponent], regularComponents: [ASTComponent], with pluginExtensions: [PluginExtension], _ componentExtensions: [ASTComponentExtension], _ dependencies: [Dependency], _ imports: Set<String>, validate initsSourceUrlContents: [UrlFileContent], using executor: SequenceExecutor, with timeout: TimeInterval) throws -> ([Component], [PluginizedComponent], [String], String, Set<String>) {
let allComponents = commonComponentModel(of: pluginizedComponents, regularComponents: nonCoreComponents + regularComponents)
let processors: [Processor] = [
DuplicateValidator(components: allComponents, dependencies: dependencies),
ComponentConsolidator(components: allComponents, componentExtensions: componentExtensions),
ParentLinker(components: allComponents),
DependencyLinker(components: allComponents, dependencies: dependencies),
NonCoreComponentLinker(pluginizedComponents: pluginizedComponents, nonCoreComponents: nonCoreComponents),
PluginExtensionLinker(pluginizedComponents: pluginizedComponents, pluginExtensions: pluginExtensions),
AncestorCycleValidator(components: allComponents),
PluginExtensionCycleValidator(pluginizedComponents: pluginizedComponents),
ComponentInstantiationValidator(components: allComponents, urlFileContents: initsSourceUrlContents, executor: executor, timeout: timeout)
]
for processor in processors {
try processor.process()
}
// Return back non-core components as well since they can be treated as any regular component.
let valueTypeComponents = (regularComponents + nonCoreComponents).map { (astComponent: ASTComponent) -> Component in
astComponent.valueType
}
let valueTypePluginizedComponents = pluginizedComponents.map { (astComponent: PluginizedASTComponent) -> PluginizedComponent in
return astComponent.valueType
}
let sortedImports = imports.sorted()
let needleVersionHash = createNeedleHash(dependencies: dependencies,
components: (regularComponents + nonCoreComponents),
pluginizedComponents: pluginizedComponents)
let inputs = createInputsSet(dependencies: dependencies,
components: (regularComponents + nonCoreComponents),
pluginizedComponents: pluginizedComponents)
return (valueTypeComponents, valueTypePluginizedComponents, sortedImports, needleVersionHash, inputs)
}
private func commonComponentModel(of pluginizedComponents: [PluginizedASTComponent], regularComponents: [ASTComponent]) -> [ASTComponent] {
let pluginizedComponentData = pluginizedComponents.map { (component: PluginizedASTComponent) -> ASTComponent in
component.data
}
return regularComponents + pluginizedComponentData
}
private func createNeedleHash(dependencies: [Dependency],
components: [ASTComponent],
pluginizedComponents: [PluginizedASTComponent]) -> String {
var hashEntries : Set<HashEntry> = []
hashEntries.formUnion(dependencies.map({ HashEntry(name: $0.name, hash: $0.sourceHash) }))
hashEntries.formUnion(pluginizedComponents.map({ HashEntry(name: $0.data.name, hash: $0.data.sourceHash) }))
hashEntries.formUnion(components.map({ HashEntry(name: $0.name, hash: $0.sourceHash) }))
return generateCumulativeHash(hashEntries: hashEntries)
}
private func createInputsSet(dependencies: [Dependency],
components: [ASTComponent],
pluginizedComponents: [PluginizedASTComponent]) -> Set<String> {
var inputs : Set<String> = []
inputs.formUnion(dependencies.map({ $0.filePath }))
inputs.formUnion(pluginizedComponents.map({ $0.data.filePath }))
inputs.formUnion(components.map({ $0.filePath }))
return inputs
}
}
private typealias DependencyNodeUrlSequenceHandle = (handle: SequenceExecutionHandle<PluginizedDependencyGraphNode>, fileUrl: URL)