Generator/Sources/NeedleFramework/Parsing/Processors/ComponentInstantiationValidator.swift (69 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
typealias UrlFileContent = (url: URL, content: String)
/// A post processing utility class that checks if any components are
/// instantiated incorrectly.
class ComponentInstantiationValidator: Processor {
/// Initializer.
///
/// - parameter components: The list of components that are parsed out.
/// - parameter fileContents: The list of all parsed source files.
/// - parameter executor: The execution to use for executing validation
/// tasks.
/// - parameter timeout: The timeout value to use to wait for each
/// individual validations.
init(components: [ASTComponent], urlFileContents: [UrlFileContent], executor: SequenceExecutor, timeout: TimeInterval) {
self.componentNames = Set(components.map({ (component: ASTComponent) -> String in
component.name
}))
self.urlFileContents = urlFileContents
self.executor = executor
self.timeout = timeout
}
/// Process the data models.
///
/// - throws: `ProcessingError` if any component instantiation is
/// invalid.
func process() throws {
// Enqueue validation tasks.
var handles = [SequenceExecutionHandle<ComponentInstantiationValidationResult>]()
for urlFileContent in urlFileContents {
let task = ComponentInstantiationValidationTask(urlFileContent: urlFileContent, componentNames: componentNames)
let handle = executor.executeSequence(from: task) { (_, result: Any) -> SequenceExecution<ComponentInstantiationValidationResult> in
SequenceExecution.endOfSequence(result as! ComponentInstantiationValidationResult)
}
handles.append(handle)
}
// Process validation results.
for handle in handles {
let result = try handle.await(withTimeout: timeout)
switch result {
case .success:
break
case .failure(let url, let componentName):
throw GenericError.withMessage("\(componentName) is instantiated incorrectly in \(url). All components must be instantiated by parent components, by passing `self` as the argument to the parent parameter.")
}
}
}
// MARK - Private
private let componentNames: Set<String>
private let urlFileContents: [UrlFileContent]
private let executor: SequenceExecutor
private let timeout: TimeInterval
}
private enum ComponentInstantiationValidationResult {
case success
case failure(URL, String)
}
private class ComponentInstantiationValidationTask: AbstractTask<ComponentInstantiationValidationResult> {
fileprivate init(urlFileContent: UrlFileContent, componentNames: Set<String>) {
self.urlFileContent = urlFileContent
self.componentNames = componentNames
}
fileprivate override func execute() throws -> ComponentInstantiationValidationResult {
let matches = componentInstantiationRegex.matches(in: urlFileContent.content)
for match in matches {
let componentName = urlFileContent.content.substring(with: match.range(at: 1))
if let componentName = componentName, componentNames.contains(componentName) {
let matchRange = match.range(at: 0)
// Use match range + 5 as the range to extract the argument.
// This includes one extra character after the expected `self`
// argument to check for cases such as `self.blah`.
let argRange = NSRange(location: matchRange.location + matchRange.length, length: 5)
let arg = urlFileContent.content.substring(with: argRange)?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
// Special case for root component, where it is instantiated
// with BootstrapComponent().
let rootArgRange = NSRange(location: matchRange.location + matchRange.length, length: 20)
let rootArg = urlFileContent.content.substring(with: rootArgRange)?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if let arg = arg, !validate(componentInstantiationArg: arg, and: rootArg) {
return .failure(urlFileContent.url, componentName)
}
}
}
return .success
}
// MARK: - Private
private let urlFileContent: UrlFileContent
private let componentNames: Set<String>
private func validate(componentInstantiationArg arg: String, and rootArg: String?) -> Bool {
return arg == "self" || arg == "self)" || arg == "self," || rootArg == "BootstrapComponent()"
}
}