diff --git a/Package.swift b/Package.swift index c3dd95147..854b186ad 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,11 @@ let package = Package( ), .target( name: "SwiftFormatTestSupport", - dependencies: ["SwiftFormatCore", "SwiftFormatConfiguration"] + dependencies: [ + "SwiftFormatCore", + "SwiftFormatRules", + "SwiftFormatConfiguration", + ] ), .target( name: "SwiftFormatWhitespaceLinter", diff --git a/Sources/SwiftFormat/LintPipeline.swift b/Sources/SwiftFormat/LintPipeline.swift index 3870de0da..2921975e4 100644 --- a/Sources/SwiftFormat/LintPipeline.swift +++ b/Sources/SwiftFormat/LintPipeline.swift @@ -29,7 +29,7 @@ extension LintPipeline { func visitIfEnabled( _ visitor: (Rule) -> (Node) -> SyntaxVisitorContinueKind, for node: Node ) { - guard context.isRuleEnabled(Rule.self.ruleName, node: Syntax(node)) else { return } + guard context.isRuleEnabled(Rule.self, node: Syntax(node)) else { return } let rule = self.rule(Rule.self) _ = visitor(rule)(node) } @@ -50,7 +50,7 @@ extension LintPipeline { // more importantly because the `visit` methods return protocol refinements of `Syntax` that // cannot currently be expressed as constraints without duplicating this function for each of // them individually. - guard context.isRuleEnabled(Rule.self.ruleName, node: Syntax(node)) else { return } + guard context.isRuleEnabled(Rule.self, node: Syntax(node)) else { return } let rule = self.rule(Rule.self) _ = visitor(rule)(node) } diff --git a/Sources/SwiftFormat/SwiftFormatter.swift b/Sources/SwiftFormat/SwiftFormatter.swift index c1862d757..36de15be3 100644 --- a/Sources/SwiftFormat/SwiftFormatter.swift +++ b/Sources/SwiftFormat/SwiftFormatter.swift @@ -14,6 +14,7 @@ import Foundation import SwiftFormatConfiguration import SwiftFormatCore import SwiftFormatPrettyPrint +import SwiftFormatRules import SwiftSyntax /// Formats Swift source code or syntax trees according to the Swift style guidelines. @@ -108,7 +109,7 @@ public final class SwiftFormatter { let assumedURL = url ?? URL(fileURLWithPath: "source") let context = Context( configuration: configuration, diagnosticEngine: diagnosticEngine, fileURL: assumedURL, - sourceFileSyntax: syntax, source: source) + sourceFileSyntax: syntax, source: source, ruleNameCache: ruleNameCache) let pipeline = FormatPipeline(context: context) let transformedSyntax = pipeline.visit(Syntax(syntax)) diff --git a/Sources/SwiftFormat/SwiftLinter.swift b/Sources/SwiftFormat/SwiftLinter.swift index ceb00833f..332642213 100644 --- a/Sources/SwiftFormat/SwiftLinter.swift +++ b/Sources/SwiftFormat/SwiftLinter.swift @@ -14,6 +14,7 @@ import Foundation import SwiftFormatConfiguration import SwiftFormatCore import SwiftFormatPrettyPrint +import SwiftFormatRules import SwiftFormatWhitespaceLinter import SwiftSyntax @@ -88,7 +89,7 @@ public final class SwiftLinter { let context = Context( configuration: configuration, diagnosticEngine: diagnosticEngine, fileURL: url, - sourceFileSyntax: syntax, source: source) + sourceFileSyntax: syntax, source: source, ruleNameCache: ruleNameCache) let pipeline = LintPipeline(context: context) pipeline.walk(Syntax(syntax)) diff --git a/Sources/SwiftFormatCore/Context.swift b/Sources/SwiftFormatCore/Context.swift index 575405201..68f3b7916 100644 --- a/Sources/SwiftFormatCore/Context.swift +++ b/Sources/SwiftFormatCore/Context.swift @@ -52,13 +52,17 @@ public class Context { /// Contains the rules have been disabled by comments for certain line numbers. public let ruleMask: RuleMask + /// Contains all the available rules' names associated to their types' object identifiers. + public let ruleNameCache: [ObjectIdentifier: String] + /// Creates a new Context with the provided configuration, diagnostic engine, and file URL. public init( configuration: Configuration, diagnosticEngine: DiagnosticEngine?, fileURL: URL, sourceFileSyntax: SourceFileSyntax, - source: String? = nil + source: String? = nil, + ruleNameCache: [ObjectIdentifier: String] ) { self.configuration = configuration self.diagnosticEngine = diagnosticEngine @@ -71,12 +75,22 @@ public class Context { syntaxNode: Syntax(sourceFileSyntax), sourceLocationConverter: sourceLocationConverter ) + self.ruleNameCache = ruleNameCache } /// Given a rule's name and the node it is examining, determine if the rule is disabled at this /// location or not. - public func isRuleEnabled(_ ruleName: String, node: Syntax) -> Bool { + public func isRuleEnabled(_ rule: R.Type, node: Syntax) -> Bool { let loc = node.startLocation(converter: self.sourceLocationConverter) + + assert( + ruleNameCache[ObjectIdentifier(rule)] != nil, + """ + Missing cached rule name for '\(rule)'! \ + Ensure `generate-pipelines` has been run and `ruleNameCache` was injected. + """) + + let ruleName = ruleNameCache[ObjectIdentifier(rule)] ?? R.ruleName switch ruleMask.ruleState(ruleName, at: loc) { case .default: return configuration.rules[ruleName] ?? false diff --git a/Sources/SwiftFormatCore/Rule.swift b/Sources/SwiftFormatCore/Rule.swift index 887092f8e..52244a644 100644 --- a/Sources/SwiftFormatCore/Rule.swift +++ b/Sources/SwiftFormatCore/Rule.swift @@ -17,7 +17,7 @@ public protocol Rule { /// The context in which the rule is executed. var context: Context { get } - /// The human-readable name of the rule. This defaults to the class name. + /// The human-readable name of the rule. This defaults to the type name. static var ruleName: String { get } /// Whether this rule is opt-in, meaning it is disabled by default. @@ -27,27 +27,7 @@ public protocol Rule { init(context: Context) } -fileprivate var nameCache = [ObjectIdentifier: String]() -fileprivate var nameCacheQueue = DispatchQueue( - label: "com.apple.SwiftFormat.NameCache", attributes: .concurrent) - extension Rule { /// By default, the `ruleName` is just the name of the implementing rule class. - public static var ruleName: String { - let identifier = ObjectIdentifier(self) - let cachedName = nameCacheQueue.sync { - nameCache[identifier] - } - - if let cachedName = cachedName { - return cachedName - } - - let name = String("\(self)".split(separator: ".").last!) - nameCacheQueue.async(flags: .barrier) { - nameCache[identifier] = name - } - - return name - } + public static var ruleName: String { String("\(self)".split(separator: ".").last!) } } diff --git a/Sources/SwiftFormatCore/SyntaxFormatRule.swift b/Sources/SwiftFormatCore/SyntaxFormatRule.swift index 42a771a05..4ceb5ce61 100644 --- a/Sources/SwiftFormatCore/SyntaxFormatRule.swift +++ b/Sources/SwiftFormatCore/SyntaxFormatRule.swift @@ -31,7 +31,7 @@ open class SyntaxFormatRule: SyntaxRewriter, Rule { open override func visitAny(_ node: Syntax) -> Syntax? { // If the rule is not enabled, then return the node unmodified; otherwise, returning nil tells // SwiftSyntax to continue with the standard dispatch. - guard context.isRuleEnabled(Self.ruleName, node: node) else { return node } + guard context.isRuleEnabled(type(of: self), node: node) else { return node } return nil } } diff --git a/Sources/SwiftFormatRules/OrderedImports.swift b/Sources/SwiftFormatRules/OrderedImports.swift index 080fa4639..bf6123049 100644 --- a/Sources/SwiftFormatRules/OrderedImports.swift +++ b/Sources/SwiftFormatRules/OrderedImports.swift @@ -324,7 +324,7 @@ fileprivate func generateLines(codeBlockItemList: CodeBlockItemListSyntax, conte lines.append(currentLine) currentLine = Line() } - let sortable = context.isRuleEnabled(OrderedImports.ruleName, node: Syntax(block)) + let sortable = context.isRuleEnabled(OrderedImports.self, node: Syntax(block)) currentLine.syntaxNode = .importCodeBlock(block, sortable: sortable) } else { guard let syntaxNode = currentLine.syntaxNode else { diff --git a/Sources/SwiftFormatRules/RuleNameCache+Generated.swift b/Sources/SwiftFormatRules/RuleNameCache+Generated.swift new file mode 100644 index 000000000..56f5c3a12 --- /dev/null +++ b/Sources/SwiftFormatRules/RuleNameCache+Generated.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +// This file is automatically generated with generate-pipeline. Do Not Edit! + +/// By default, the `Rule.ruleName` should be the name of the implementing rule type. +public let ruleNameCache: [ObjectIdentifier: String] = [ + ObjectIdentifier(AllPublicDeclarationsHaveDocumentation.self): "AllPublicDeclarationsHaveDocumentation", + ObjectIdentifier(AlwaysUseLowerCamelCase.self): "AlwaysUseLowerCamelCase", + ObjectIdentifier(AmbiguousTrailingClosureOverload.self): "AmbiguousTrailingClosureOverload", + ObjectIdentifier(BeginDocumentationCommentWithOneLineSummary.self): "BeginDocumentationCommentWithOneLineSummary", + ObjectIdentifier(DoNotUseSemicolons.self): "DoNotUseSemicolons", + ObjectIdentifier(DontRepeatTypeInStaticProperties.self): "DontRepeatTypeInStaticProperties", + ObjectIdentifier(FileScopedDeclarationPrivacy.self): "FileScopedDeclarationPrivacy", + ObjectIdentifier(FullyIndirectEnum.self): "FullyIndirectEnum", + ObjectIdentifier(GroupNumericLiterals.self): "GroupNumericLiterals", + ObjectIdentifier(IdentifiersMustBeASCII.self): "IdentifiersMustBeASCII", + ObjectIdentifier(NeverForceUnwrap.self): "NeverForceUnwrap", + ObjectIdentifier(NeverUseForceTry.self): "NeverUseForceTry", + ObjectIdentifier(NeverUseImplicitlyUnwrappedOptionals.self): "NeverUseImplicitlyUnwrappedOptionals", + ObjectIdentifier(NoAccessLevelOnExtensionDeclaration.self): "NoAccessLevelOnExtensionDeclaration", + ObjectIdentifier(NoBlockComments.self): "NoBlockComments", + ObjectIdentifier(NoCasesWithOnlyFallthrough.self): "NoCasesWithOnlyFallthrough", + ObjectIdentifier(NoEmptyTrailingClosureParentheses.self): "NoEmptyTrailingClosureParentheses", + ObjectIdentifier(NoLabelsInCasePatterns.self): "NoLabelsInCasePatterns", + ObjectIdentifier(NoLeadingUnderscores.self): "NoLeadingUnderscores", + ObjectIdentifier(NoParensAroundConditions.self): "NoParensAroundConditions", + ObjectIdentifier(NoVoidReturnOnFunctionSignature.self): "NoVoidReturnOnFunctionSignature", + ObjectIdentifier(OneCasePerLine.self): "OneCasePerLine", + ObjectIdentifier(OneVariableDeclarationPerLine.self): "OneVariableDeclarationPerLine", + ObjectIdentifier(OnlyOneTrailingClosureArgument.self): "OnlyOneTrailingClosureArgument", + ObjectIdentifier(OrderedImports.self): "OrderedImports", + ObjectIdentifier(ReturnVoidInsteadOfEmptyTuple.self): "ReturnVoidInsteadOfEmptyTuple", + ObjectIdentifier(UseEarlyExits.self): "UseEarlyExits", + ObjectIdentifier(UseLetInEveryBoundCaseVariable.self): "UseLetInEveryBoundCaseVariable", + ObjectIdentifier(UseShorthandTypeNames.self): "UseShorthandTypeNames", + ObjectIdentifier(UseSingleLinePropertyGetter.self): "UseSingleLinePropertyGetter", + ObjectIdentifier(UseSynthesizedInitializer.self): "UseSynthesizedInitializer", + ObjectIdentifier(UseTripleSlashForDocumentationComments.self): "UseTripleSlashForDocumentationComments", + ObjectIdentifier(UseWhereClausesInForLoops.self): "UseWhereClausesInForLoops", + ObjectIdentifier(ValidateDocumentationComments.self): "ValidateDocumentationComments", +] diff --git a/Sources/SwiftFormatTestSupport/DiagnosingTestCase.swift b/Sources/SwiftFormatTestSupport/DiagnosingTestCase.swift index 81953f55d..c7b631c12 100644 --- a/Sources/SwiftFormatTestSupport/DiagnosingTestCase.swift +++ b/Sources/SwiftFormatTestSupport/DiagnosingTestCase.swift @@ -1,5 +1,6 @@ import SwiftFormatConfiguration import SwiftFormatCore +import SwiftFormatRules import SwiftSyntax import XCTest @@ -39,7 +40,8 @@ open class DiagnosingTestCase: XCTestCase { configuration: configuration ?? Configuration(), diagnosticEngine: DiagnosticEngine(), fileURL: URL(fileURLWithPath: "/tmp/test.swift"), - sourceFileSyntax: sourceFileSyntax) + sourceFileSyntax: sourceFileSyntax, + ruleNameCache: ruleNameCache) consumer = DiagnosticTrackingConsumer() context.diagnosticEngine?.addConsumer(consumer) return context diff --git a/Sources/generate-pipeline/RuleNameCacheGenerator.swift b/Sources/generate-pipeline/RuleNameCacheGenerator.swift new file mode 100644 index 000000000..6c7e3a729 --- /dev/null +++ b/Sources/generate-pipeline/RuleNameCacheGenerator.swift @@ -0,0 +1,55 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation + +/// Generates the rule registry file used to populate the default configuration. +final class RuleNameCacheGenerator: FileGenerator { + + /// The rules collected by scanning the formatter source code. + let ruleCollector: RuleCollector + + /// Creates a new rule registry generator. + init(ruleCollector: RuleCollector) { + self.ruleCollector = ruleCollector + } + + func write(into handle: FileHandle) throws { + handle.write( + """ + //===----------------------------------------------------------------------===// + // + // This source file is part of the Swift.org open source project + // + // Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors + // Licensed under Apache License v2.0 with Runtime Library Exception + // + // See https://swift.org/LICENSE.txt for license information + // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors + // + //===----------------------------------------------------------------------===// + + // This file is automatically generated with generate-pipeline. Do Not Edit! + + /// By default, the `Rule.ruleName` should be the name of the implementing rule type. + public let ruleNameCache: [ObjectIdentifier: String] = [ + + """ + ) + + for detectedRule in ruleCollector.allLinters.sorted(by: { $0.typeName < $1.typeName }) { + handle.write(" ObjectIdentifier(\(detectedRule.typeName).self): \"\(detectedRule.typeName)\",\n") + } + handle.write("]\n") + } +} + diff --git a/Sources/generate-pipeline/main.swift b/Sources/generate-pipeline/main.swift index 5fd747705..afc469ad9 100644 --- a/Sources/generate-pipeline/main.swift +++ b/Sources/generate-pipeline/main.swift @@ -24,6 +24,10 @@ let ruleRegistryFile = sourcesDirectory .appendingPathComponent("SwiftFormatConfiguration") .appendingPathComponent("RuleRegistry+Generated.swift") +let ruleNameCacheFile = sourcesDirectory + .appendingPathComponent("SwiftFormatRules") + .appendingPathComponent("RuleNameCache+Generated.swift") + var ruleCollector = RuleCollector() try ruleCollector.collect(from: rulesDirectory) @@ -34,3 +38,7 @@ try pipelineGenerator.generateFile(at: pipelineFile) // Generate the rule registry dictionary for configuration. let registryGenerator = RuleRegistryGenerator(ruleCollector: ruleCollector) try registryGenerator.generateFile(at: ruleRegistryFile) + +// Generate the rule name cache. +let ruleNameCacheGenerator = RuleNameCacheGenerator(ruleCollector: ruleCollector) +try ruleNameCacheGenerator.generateFile(at: ruleNameCacheFile) diff --git a/Sources/swift-format/Frontend/Frontend.swift b/Sources/swift-format/Frontend/Frontend.swift index fce1563ca..8dedf9173 100644 --- a/Sources/swift-format/Frontend/Frontend.swift +++ b/Sources/swift-format/Frontend/Frontend.swift @@ -130,35 +130,31 @@ class Frontend { "processPaths(_:) should only be called when paths is non-empty.") if parallel { - let allFilePaths = Array(FileIterator(paths: paths)) - DispatchQueue.concurrentPerform(iterations: allFilePaths.count) { index in - let path = allFilePaths[index] - openAndProcess(path) + let filesToProcess = FileIterator(paths: paths).compactMap(openAndPrepareFile) + DispatchQueue.concurrentPerform(iterations: filesToProcess.count) { index in + processFile(filesToProcess[index]) } } else { - for path in FileIterator(paths: paths) { - openAndProcess(path) - } + FileIterator(paths: paths).lazy.compactMap(openAndPrepareFile).forEach(processFile) } } - /// Read and process the given path, optionally synchronizing diagnostic output. - private func openAndProcess(_ path: String) -> Void { + /// Read and prepare the file at the given path for processing, optionally synchronizing + /// diagnostic output. + private func openAndPrepareFile(atPath path: String) -> FileToProcess? { guard let sourceFile = FileHandle(forReadingAtPath: path) else { diagnosticEngine.diagnose(Diagnostic.Message(.error, "Unable to open \(path)")) - return + return nil } guard let configuration = configuration( atPath: lintFormatOptions.configurationPath, orInferredFromSwiftFileAtPath: path) else { // Already diagnosed in the called method. - return + return nil } - let fileToProcess = FileToProcess( - fileHandle: sourceFile, path: path, configuration: configuration) - processFile(fileToProcess) + return FileToProcess(fileHandle: sourceFile, path: path, configuration: configuration) } /// Returns the configuration that applies to the given `.swift` source file, when an explicit