Generator/Sources/NeedleFramework/Parsing/AbstractDependencyGraphParser.swift (113 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 /// Errors that can occur during parsing of the dependency graph from /// Swift sources. enum DependencyGraphParserError: Error { /// Parsing a particular source file timed out. The associated values /// are the path of the file being parsed and the ID of the task that /// was being executed when the timeout occurred. case timeout(String, Int) } /// The base implementation of parsing a set of Swift source files for /// dependency graph models. class AbstractDependencyGraphParser { private var allFileUrls: [URL] = [] /// Execute a set of tasks on the files within the given root URLs /// and return their execution handles. /// /// - 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 execution: The logic to invoke for each file. /// - returns: The set of execution handles returned by the given logic /// closure and their corresponding file URLs. /// - throws: If any error occurred during execution. func executeAndCollectTaskHandles<ResultType>(with rootUrls: [URL], sourcesListFormatValue: String?, execution: (URL) -> SequenceExecutionHandle<ResultType>) throws -> [(SequenceExecutionHandle<ResultType>, URL)] { var urlHandles = [(SequenceExecutionHandle<ResultType>, URL)]() // Enumerate all files and execute parsing sequences concurrently. try collectFileUrlsIfNeeded(with: rootUrls, sourcesListFormatValue: sourcesListFormatValue) for fileUrl in allFileUrls { let taskHandle = execution(fileUrl) urlHandles.append((taskHandle, fileUrl)) } return urlHandles } private func collectFileUrlsIfNeeded(with rootUrls: [URL], sourcesListFormatValue: String?) throws { guard allFileUrls.isEmpty else { return } let enumerator = FileEnumerator() for url in rootUrls { try enumerator.enumerate(from: url, withSourcesListFormat: sourcesListFormatValue) { (fileUrl: URL) in allFileUrls.append(fileUrl) } } } // MARK: - Extension Parsing /// Parse source files in directories specified by the given root URLs /// for component extension data models. /// /// - 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 parsedComponents: The components that are parsed out /// from declarations, whose extensions this method parses. /// - 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 extensions for parsed components /// and their import statements. /// - throws: If any error occurred during execution. func parseAndCollectComponentExtensionDataModels(with rootUrls: [URL], sourcesListFormatValue: String?, excludingFilesEndingWith exclusionSuffixes: [String], excludingFilesWithPaths exclusionPaths: [String], parsedComponents: [ASTComponent], using executor: SequenceExecutor, with timeout: TimeInterval) throws -> ([ASTComponentExtension], Set<String>) { let componentExtensionUrlHandles: [ComponentExtensionsUrlSequenceHandle] = try enqueueExtensionParsingTasks(with: rootUrls, sourcesListFormatValue: sourcesListFormatValue, excludingFilesEndingWith: exclusionSuffixes, excludingFilesWithPaths: exclusionPaths, parsedComponents: parsedComponents, using: executor) return try collectExtensionDataModels(with: componentExtensionUrlHandles, waitUpTo: timeout) } private func enqueueExtensionParsingTasks(with rootUrls: [URL], sourcesListFormatValue: String?, excludingFilesEndingWith exclusionSuffixes: [String], excludingFilesWithPaths exclusionPaths: [String], parsedComponents: [ASTComponent], using executor: SequenceExecutor) throws -> [ComponentExtensionsUrlSequenceHandle] { return try executeAndCollectTaskHandles(with: rootUrls, sourcesListFormatValue: sourcesListFormatValue) { (fileUrl: URL) -> SequenceExecutionHandle<ComponentExtensionNode> in let task = ComponentExtensionsFilterTask(url: fileUrl, exclusionSuffixes: exclusionSuffixes, exclusionPaths: exclusionPaths, components: parsedComponents) return executor.executeSequence(from: task) { (currentTask: Task, currentResult: Any) -> SequenceExecution<ComponentExtensionNode> in if currentTask is ComponentExtensionsFilterTask, let filterResult = currentResult as? FilterResult { switch filterResult { case .shouldProcess(let url, let content): return .continueSequence(ASTProducerTask(sourceUrl: url, sourceContent: content)) case .skip: return .endOfSequence(ComponentExtensionNode(extensions: [], imports: [])) } } else if currentTask is ASTProducerTask, let ast = currentResult as? AST { return .continueSequence(ComponentExtensionsParserTask(ast: ast, components: parsedComponents)) } else if currentTask is ComponentExtensionsParserTask, let node = currentResult as? ComponentExtensionNode { return .endOfSequence(node) } else { error("Unhandled task \(currentTask) with result \(currentResult)") } } } } private func collectExtensionDataModels(with urlHandles: [ComponentExtensionsUrlSequenceHandle], waitUpTo timeout: TimeInterval) throws -> ([ASTComponentExtension], Set<String>) { var extensions = [ASTComponentExtension]() var imports = Set<String>() for urlHandle in urlHandles { do { let node = try urlHandle.handle.await(withTimeout: timeout) extensions.append(contentsOf: node.extensions) // Ignore imports if we don't find anything useful in this file if !node.extensions.isEmpty { for statement in node.imports { imports.insert(statement) } } } catch SequenceExecutionError.awaitTimeout(let taskId) { throw DependencyGraphParserError.timeout(urlHandle.fileUrl.absoluteString, taskId) } catch { throw error } } return (extensions, imports) } // MARK: - Component Initializer Parsing /// Collect all the source file contents that contain component /// instantiations from the given root URLs. /// /// - 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 source file URL and content pairs. /// - throws: If any error occurred during execution. func sourceUrlContentsContainComponentInstantiations(with rootUrls: [URL], sourcesListFormatValue: String?, excludingFilesEndingWith exclusionSuffixes: [String], excludingFilesWithPaths exclusionPaths: [String], using executor: SequenceExecutor, with timeout: TimeInterval) throws -> [UrlFileContent] { let initsUrlHandles = try enqueueComponentInitsTasks(with: rootUrls, sourcesListFormatValue: sourcesListFormatValue, excludingFilesEndingWith: exclusionSuffixes, excludingFilesWithPaths: exclusionPaths, using: executor) return try collectInitsDataModels(with: initsUrlHandles, waitUpTo: timeout) } private func enqueueComponentInitsTasks(with rootUrls: [URL], sourcesListFormatValue: String?, excludingFilesEndingWith exclusionSuffixes: [String], excludingFilesWithPaths exclusionPaths: [String], using executor: SequenceExecutor) throws -> [ComponentInitsUrlSequenceHandle] { return try executeAndCollectTaskHandles(with: rootUrls, sourcesListFormatValue: sourcesListFormatValue) { (fileUrl: URL) -> SequenceExecutionHandle<UrlFileContent?> in let task = ComponentInitsFilterTask(url: fileUrl, exclusionSuffixes: exclusionSuffixes, exclusionPaths: exclusionPaths) return executor.executeSequence(from: task) { (currentTask: Task, currentResult: Any) -> SequenceExecution<UrlFileContent?> in if currentTask is ComponentInitsFilterTask, let filterResult = currentResult as? FilterResult { switch filterResult { case .shouldProcess(let url, let content): return .endOfSequence((url, content)) case .skip: return .endOfSequence(nil) } } else { error("Unhandled task \(currentTask) with result \(currentResult)") } } } } private func collectInitsDataModels(with urlHandles: [ComponentInitsUrlSequenceHandle], waitUpTo timeout: TimeInterval) throws -> [UrlFileContent] { var sourceUrlContents = [UrlFileContent]() for urlHandle in urlHandles { do { let urlContent = try urlHandle.handle.await(withTimeout: timeout) if let urlContent = urlContent { sourceUrlContents.append(urlContent) } } catch SequenceExecutionError.awaitTimeout(let taskId) { throw DependencyGraphParserError.timeout(urlHandle.fileUrl.absoluteString, taskId) } catch { throw error } } return sourceUrlContents } } private typealias ComponentExtensionsUrlSequenceHandle = (handle: SequenceExecutionHandle<ComponentExtensionNode>, fileUrl: URL) private typealias ComponentInitsUrlSequenceHandle = (handle: SequenceExecutionHandle<UrlFileContent?>, fileUrl: URL)