Skip to content

Commit 5652419

Browse files
authored
Merge pull request swiftlang#16 from allevato/pipeline
Lint pipeline improvements; fix enabling/disabling of lint rules.
2 parents 730943f + a72231e commit 5652419

29 files changed

+354
-342
lines changed

Sources/SwiftFormat/LintPipeline.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,42 @@ struct LintPipeline: SyntaxVisitor {
2525
init(context: Context) {
2626
self.context = context
2727
}
28+
29+
/// Calls the `visit` method of a rule for the given node if that rule is enabled for the node.
30+
///
31+
/// - Parameters:
32+
/// - visitor: A reference to the `visit` method on the *type* of a `SyntaxLintRule` subclass.
33+
/// The type of the rule in question is inferred from the signature of the method reference.
34+
/// - context: The formatter context that contains information about which rules are enabled or
35+
/// disabled.
36+
/// - node: The syntax node on which the rule will be applied. This lets us check whether the
37+
/// rule is enabled for the particular source range where the node occurs.
38+
func visitIfEnabled<Rule: SyntaxLintRule, Node: Syntax>(
39+
_ visitor: (Rule) -> (Node) -> SyntaxVisitorContinueKind, in context: Context, for node: Node
40+
) {
41+
guard context.isRuleEnabled(Rule.self.ruleName, node: node) else { return }
42+
let rule = Rule(context: context)
43+
_ = visitor(rule)(node)
44+
}
45+
46+
/// Calls the `visit` method of a rule for the given node if that rule is enabled for the node.
47+
///
48+
/// - Parameters:
49+
/// - visitor: A reference to the `visit` method on the *type* of a `SyntaxFormatRule` subclass.
50+
/// The type of the rule in question is inferred from the signature of the method reference.
51+
/// - context: The formatter context that contains information about which rules are enabled or
52+
/// disabled.
53+
/// - node: The syntax node on which the rule will be applied. This lets us check whether the
54+
/// rule is enabled for the particular source range where the node occurs.
55+
func visitIfEnabled<Rule: SyntaxFormatRule, Node: Syntax>(
56+
_ visitor: (Rule) -> (Node) -> Any, in context: Context, for node: Node
57+
) {
58+
// Note that visitor function type is expressed as `Any` because we ignore the return value, but
59+
// more importantly because the `visit` methods return protocol refinements of `Syntax` that
60+
// cannot currently be expressed as constraints without duplicating this function for each of
61+
// them individually.
62+
guard context.isRuleEnabled(Rule.self.ruleName, node: node) else { return }
63+
let rule = Rule(context: context)
64+
_ = visitor(rule)(node)
65+
}
2866
}

Sources/SwiftFormat/Pipelines+Generated.swift

Lines changed: 96 additions & 96 deletions
Large diffs are not rendered by default.

Sources/SwiftFormatCore/Context.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,16 @@ public class Context {
7373

7474
/// Given a rule's name and the node it is examining, determine if the rule is disabled at this
7575
/// location or not.
76-
public func isRuleDisabled(_ ruleName: String, node: Syntax) -> Bool {
76+
public func isRuleEnabled(_ ruleName: String, node: Syntax) -> Bool {
7777
let loc = node.startLocation(converter: self.sourceLocationConverter)
7878
guard let line = loc.line else { return false }
79-
return self.ruleMask.isDisabled(ruleName, line: line)
79+
switch ruleMask.ruleState(ruleName, atLine: line) {
80+
case .default:
81+
return configuration.rules[ruleName] ?? false
82+
case .disabled:
83+
return false
84+
case .enabled:
85+
return true
86+
}
8087
}
8188
}

Sources/SwiftFormatCore/Diagnostic+Rule.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ extension Diagnostic.Message {
1717
/// - parameter rule: The rule whose name will be prepended to the diagnostic.
1818
/// - returns: A new `Diagnostic.Message` with the name of the provided rule prepended.
1919
public func withRule(_ rule: Rule) -> Diagnostic.Message {
20-
return .init(severity, "[\(rule.ruleName)]: \(text)")
20+
return .init(severity, "[\(type(of: rule).ruleName)]: \(text)")
2121
}
2222
}

Sources/SwiftFormatCore/Rule.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public protocol Rule {
1616
var context: Context { get }
1717

1818
/// The human-readable name of the rule. This defaults to the class name.
19-
var ruleName: String { get }
19+
static var ruleName: String { get }
2020

2121
/// Creates a new Rule in a given context.
2222
init(context: Context)
@@ -26,12 +26,10 @@ private var nameCache = [ObjectIdentifier: String]()
2626

2727
extension Rule {
2828
/// By default, the `ruleName` is just the name of the implementing rule class.
29-
public var ruleName: String {
30-
let myType = type(of: self)
31-
// TODO(abl): Test and potentially replace with static initialization.
29+
public static var ruleName: String {
3230
return nameCache[
33-
ObjectIdentifier(myType),
34-
default: String("\(myType)".split(separator: ".").last!)
31+
ObjectIdentifier(self),
32+
default: String("\(self)".split(separator: ".").last!)
3533
]
3634
}
3735
}

Sources/SwiftFormatCore/RuleMask.swift

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,19 @@ import SwiftSyntax
2424
/// RuleMask to see if it is disabled for the line it is currently examining.
2525
public class RuleMask {
2626

27-
/// Each rule has a list of ranges for which it is disabled.
28-
private var ruleMap: [String: [Range<Int>]] = [:]
27+
/// Information about whether a particular lint/format rule is enabled or disabled for a range of
28+
/// lines in the source file.
29+
private struct LineRangeState {
30+
31+
/// The line range where a rule is either disabled or enabled.
32+
var range: Range<Int>
33+
34+
/// Indicates whether the rule is enabled in this range.
35+
var isEnabled: Bool
36+
}
37+
38+
/// Each rule has a list of ranges for which it is explicitly enabled or disabled.
39+
private var ruleMap: [String: [LineRangeState]] = [:]
2940

3041
/// Regex to match the enable comments; rule name is in the first capture group.
3142
private let enablePattern = #"^\s*//\s*swift-format-enable:\s+(\S+)"#
@@ -58,8 +69,9 @@ public class RuleMask {
5869
return loc.line
5970
}
6071

61-
/// Check if a comment matches a disable/enable flag.
72+
/// Check if a comment matches a disable/enable flag, and if so, returns the name of the rule.
6273
private func getRule(regex: NSRegularExpression, text: String) -> String? {
74+
// TODO: Support multiple rules in the same comment; e.g., a comma-delimited list.
6375
let nsrange = NSRange(text.startIndex..<text.endIndex, in: text)
6476
if let match = regex.firstMatch(in: text, options: [], range: nsrange) {
6577
let matchRange = match.range(at: 1)
@@ -73,47 +85,73 @@ public class RuleMask {
7385
/// Generate the dictionary (ruleMap) by walking the syntax tokens.
7486
private func generateDictionary(_ node: Syntax) {
7587
var disableStart: [String: Int] = [:]
88+
var enableStart: [String: Int] = [:]
89+
7690
for token in node.tokens {
77-
guard let leadingtrivia = token.leadingTrivia else { continue }
91+
guard let leadingTrivia = token.leadingTrivia else { continue }
7892

7993
// Flags must be on lines by themselves: not at the end of an existing line.
8094
var firstPiece = true
8195

82-
for piece in leadingtrivia {
96+
for piece in leadingTrivia {
8397
guard case .lineComment(let text) = piece else {
8498
firstPiece = false
8599
continue
86100
}
87101
guard !firstPiece else { continue }
88102

89-
if let disableRule = getRule(regex: disableRegex, text: text) {
90-
guard !disableStart.keys.contains(disableRule) else { continue }
91-
guard let startLine = getLine(token) else { continue }
92-
disableStart[disableRule] = startLine
93-
}
103+
if let ruleName = getRule(regex: disableRegex, text: text) {
104+
guard !disableStart.keys.contains(ruleName) else { continue }
105+
guard let disableStartLine = getLine(token) else { continue }
94106

95-
if let enableRule = getRule(regex: enableRegex, text: text) {
96-
guard let startLine = disableStart.removeValue(forKey: enableRule) else { continue }
97-
guard let endLine = getLine(token) else { continue }
98-
let exclusionRange = startLine..<endLine
107+
if let enableStartLine = enableStart[ruleName] {
108+
// If we're processing an enable block for the rule, finalize it.
109+
ruleMap[ruleName, default: []].append(
110+
LineRangeState(range: enableStartLine..<disableStartLine, isEnabled: true))
111+
enableStart[ruleName] = nil
112+
}
99113

100-
if ruleMap.keys.contains(enableRule) {
101-
ruleMap[enableRule]?.append(exclusionRange)
102-
} else {
103-
ruleMap[enableRule] = [exclusionRange]
114+
disableStart[ruleName] = disableStartLine
115+
}
116+
else if let ruleName = getRule(regex: enableRegex, text: text) {
117+
guard !enableStart.keys.contains(ruleName) else { continue }
118+
guard let enableStartLine = getLine(token) else { continue }
119+
120+
if let disableStartLine = disableStart[ruleName] {
121+
// If we're processing a disable block for the rule, finalize it.
122+
ruleMap[ruleName, default: []].append(
123+
LineRangeState(range: disableStartLine..<enableStartLine, isEnabled: false))
124+
disableStart[ruleName] = nil
104125
}
126+
127+
enableStart[ruleName] = enableStartLine
105128
}
129+
106130
firstPiece = false
107131
}
108132
}
133+
134+
// Finalize any remaining blocks by closing them off at the last line number in the file.
135+
guard let lastToken = node.lastToken, let lastLine = getLine(lastToken) else { return }
136+
137+
for (ruleName, disableStartLine) in disableStart {
138+
ruleMap[ruleName, default: []].append(
139+
LineRangeState(range: disableStartLine..<lastLine + 1, isEnabled: false))
140+
}
141+
for (ruleName, enableStartLine) in enableStart {
142+
ruleMap[ruleName, default: []].append(
143+
LineRangeState(range: enableStartLine..<lastLine + 1, isEnabled: true))
144+
}
109145
}
110146

111147
/// Return if the given rule is disabled on the provided line.
112-
public func isDisabled(_ rule: String, line: Int) -> Bool {
113-
guard let ranges = ruleMap[rule] else { return false }
114-
for range in ranges {
115-
if range.contains(line) { return true }
148+
public func ruleState(_ rule: String, atLine line: Int) -> RuleState {
149+
guard let rangeStates = ruleMap[rule] else { return .default }
150+
for rangeState in rangeStates {
151+
if rangeState.range.contains(line) {
152+
return rangeState.isEnabled ? .enabled : .disabled
153+
}
116154
}
117-
return false
155+
return .default
118156
}
119157
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// The enablement of a lint/format rule based on the presence or absence of comment directives in
14+
/// the source file.
15+
public enum RuleState {
16+
17+
/// There is no explicit information in the source file about whether the rule should be enabled
18+
/// or disabled at the requested location, so the configuration default should be used.
19+
case `default`
20+
21+
/// The rule is explicitly enabled at the requested location.
22+
case enabled
23+
24+
/// The rule is explicitly disabled at the requested location.
25+
case disabled
26+
}

Sources/SwiftFormatCore/SyntaxLintRule.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,16 @@ import Foundation
1414
import SwiftSyntax
1515

1616
/// A rule that lints a given file.
17-
public protocol SyntaxLintRule: SyntaxVisitor, Rule {}
17+
open class SyntaxLintRule: SyntaxVisitorBase, Rule {
18+
19+
/// The context in which the rule is executed.
20+
public let context: Context
21+
22+
/// Creates a new rule in a given context.
23+
public required init(context: Context) {
24+
self.context = context
25+
}
26+
}
1827

1928
extension Rule {
2029
/// Emits the provided diagnostic to the diagnostic engine.

Sources/SwiftFormatRules/AllPublicDeclarationsHaveDocumentation.swift

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,55 +19,50 @@ import SwiftSyntax
1919
/// Lint: If a public declaration is missing a documentation comment, a lint error is raised.
2020
///
2121
/// - SeeAlso: https://google.github.io/swift#where-to-document
22-
public struct AllPublicDeclarationsHaveDocumentation: SyntaxLintRule {
23-
public let context: Context
22+
public final class AllPublicDeclarationsHaveDocumentation: SyntaxLintRule {
2423

25-
public init(context: Context) {
26-
self.context = context
27-
}
28-
29-
public func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
24+
public override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
3025
diagnoseMissingDocComment(node, name: node.fullDeclName, modifiers: node.modifiers)
3126
return .skipChildren
3227
}
3328

34-
public func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
29+
public override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
3530
diagnoseMissingDocComment(node, name: "init", modifiers: node.modifiers)
3631
return .skipChildren
3732
}
3833

39-
public func visit(_ node: DeinitializerDeclSyntax) -> SyntaxVisitorContinueKind {
34+
public override func visit(_ node: DeinitializerDeclSyntax) -> SyntaxVisitorContinueKind {
4035
diagnoseMissingDocComment(node, name: "deinit", modifiers: node.modifiers)
4136
return .skipChildren
4237
}
4338

44-
public func visit(_ node: SubscriptDeclSyntax) -> SyntaxVisitorContinueKind {
39+
public override func visit(_ node: SubscriptDeclSyntax) -> SyntaxVisitorContinueKind {
4540
diagnoseMissingDocComment(node, name: "subscript", modifiers: node.modifiers)
4641
return .skipChildren
4742
}
4843

49-
public func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
44+
public override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
5045
diagnoseMissingDocComment(node, name: node.identifier.text, modifiers: node.modifiers)
5146
return .skipChildren
5247
}
5348

54-
public func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
49+
public override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
5550
guard let mainBinding = node.bindings.firstAndOnly else { return .skipChildren }
5651
diagnoseMissingDocComment(node, name: "\(mainBinding.pattern)", modifiers: node.modifiers)
5752
return .skipChildren
5853
}
5954

60-
public func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
55+
public override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
6156
diagnoseMissingDocComment(node, name: node.identifier.text, modifiers: node.modifiers)
6257
return .skipChildren
6358
}
6459

65-
public func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
60+
public override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
6661
diagnoseMissingDocComment(node, name: node.identifier.text, modifiers: node.modifiers)
6762
return .skipChildren
6863
}
6964

70-
public func visit(_ node: TypealiasDeclSyntax) -> SyntaxVisitorContinueKind {
65+
public override func visit(_ node: TypealiasDeclSyntax) -> SyntaxVisitorContinueKind {
7166
diagnoseMissingDocComment(node, name: node.identifier.text, modifiers: node.modifiers)
7267
return .skipChildren
7368
}

Sources/SwiftFormatRules/AlwaysUseLowerCamelCase.swift

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,9 @@ import SwiftSyntax
2121
/// raised.
2222
///
2323
/// - SeeAlso: https://google.github.io/swift#identifiers
24-
public struct AlwaysUseLowerCamelCase: SyntaxLintRule {
25-
public let context: Context
24+
public final class AlwaysUseLowerCamelCase: SyntaxLintRule {
2625

27-
public init(context: Context) {
28-
self.context = context
29-
}
30-
31-
public func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
26+
public override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
3227
for binding in node.bindings {
3328
guard let pat = binding.pattern as? IdentifierPatternSyntax else {
3429
continue
@@ -38,12 +33,12 @@ public struct AlwaysUseLowerCamelCase: SyntaxLintRule {
3833
return .skipChildren
3934
}
4035

41-
public func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
36+
public override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
4237
diagnoseLowerCamelCaseViolations(node.identifier)
4338
return .skipChildren
4439
}
4540

46-
public func visit(_ node: EnumCaseElementSyntax) -> SyntaxVisitorContinueKind {
41+
public override func visit(_ node: EnumCaseElementSyntax) -> SyntaxVisitorContinueKind {
4742
diagnoseLowerCamelCaseViolations(node.identifier)
4843
return .skipChildren
4944
}

0 commit comments

Comments
 (0)