Interpret strings in `excluded` option of `*_name` rules as regex (#4655)

This commit is contained in:
kyounh12 2023-01-11 07:29:04 +09:00 committed by GitHub
parent 74dbd52add
commit 5ec6112ba1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 105 additions and 13 deletions

View File

@ -25,6 +25,11 @@
* Separate analyzer rules as an independent section in the rule directory of the reference.
[Ethan Wong](https://github.com/GetToSet)
[#4664](https://github.com/realm/SwiftLint/pull/4664)
* Interpret strings in `excluded` option of `identifier_name`,
`type_name` and `generic_type_name` rules as regex.
[Moly](https://github.com/kyounh12)
[#4655](https://github.com/realm/SwiftLint/pull/4655)
#### Bug Fixes

View File

@ -0,0 +1,33 @@
import Foundation
/// Represents regex from `exclude` option in `NameConfiguration`.
/// Using NSRegularExpression causes failure on equality check between two `NameConfiguration`s.
/// This class compares pattern only when checking its equality
public final class ExcludedRegexExpression: NSObject {
/// NSRegularExpression built from given pattern
public let regex: NSRegularExpression
/// Creates an `ExcludedRegexExpression` with a pattern.
///
/// - parameter pattern: The pattern string to build regex
init?(pattern: String) {
guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil }
self.regex = regex
}
// MARK: - Equality Check
/// Compares regex pattern to check equality
override public func isEqual(_ object: Any?) -> Bool {
if let object = object as? ExcludedRegexExpression {
return regex.pattern == object.regex.pattern
} else {
return false
}
}
/// Uses regex pattern as hash
override public var hash: Int {
return regex.pattern.hashValue
}
}

View File

@ -66,9 +66,7 @@ private extension GenericTypeNameRule {
override func visitPost(_ node: GenericParameterSyntax) {
let name = node.name.text
guard !configuration.excluded.contains(name) else {
return
}
guard !configuration.shouldExclude(name: name) else { return }
let allowedSymbols = configuration.allowedSymbols.union(.alphanumerics)
if !allowedSymbols.isSuperset(of: CharacterSet(charactersIn: name)) {

View File

@ -88,9 +88,7 @@ private extension TypeNameRule {
let originalName = identifier.text
let nameConfiguration = configuration.nameConfiguration
guard !nameConfiguration.excluded.contains(originalName) else {
return nil
}
guard !nameConfiguration.shouldExclude(name: originalName) else { return nil }
let name = originalName
.strippingBackticks()

View File

@ -4,14 +4,14 @@ struct NameConfiguration: RuleConfiguration, Equatable {
var consoleDescription: String {
return "(min_length) \(minLength.shortConsoleDescription), " +
"(max_length) \(maxLength.shortConsoleDescription), " +
"excluded: \(excluded.sorted()), " +
"excluded: \(excludedRegularExpressions.map { $0.regex.pattern }.sorted()), " +
"allowed_symbols: \(allowedSymbolsSet.sorted()), " +
"validates_start_with_lowercase: \(validatesStartWithLowercase)"
}
var minLength: SeverityLevelsConfiguration
var maxLength: SeverityLevelsConfiguration
var excluded: Set<String>
var excludedRegularExpressions: Set<ExcludedRegexExpression>
private var allowedSymbolsSet: Set<String>
var validatesStartWithLowercase: Bool
@ -36,7 +36,7 @@ struct NameConfiguration: RuleConfiguration, Equatable {
validatesStartWithLowercase: Bool = true) {
minLength = SeverityLevelsConfiguration(warning: minLengthWarning, error: minLengthError)
maxLength = SeverityLevelsConfiguration(warning: maxLengthWarning, error: maxLengthError)
self.excluded = Set(excluded)
self.excludedRegularExpressions = Set(excluded.compactMap { ExcludedRegexExpression(pattern: "^\($0)$") })
self.allowedSymbolsSet = Set(allowedSymbols)
self.validatesStartWithLowercase = validatesStartWithLowercase
}
@ -53,7 +53,7 @@ struct NameConfiguration: RuleConfiguration, Equatable {
try maxLength.apply(configuration: maxLengthConfiguration)
}
if let excluded = [String].array(of: configurationDict["excluded"]) {
self.excluded = Set(excluded)
self.excludedRegularExpressions = Set(excluded.compactMap { ExcludedRegexExpression(pattern: "^\($0)$") })
}
if let allowedSymbols = [String].array(of: configurationDict["allowed_symbols"]) {
self.allowedSymbolsSet = Set(allowedSymbols)
@ -90,3 +90,13 @@ extension NameConfiguration {
return nil
}
}
// MARK: - `exclude` option extensions
extension NameConfiguration {
func shouldExclude(name: String) -> Bool {
return excludedRegularExpressions.contains(where: {
return !$0.regex.matches(in: name, options: [], range: NSRange(name.startIndex..., in: name)).isEmpty
})
}
}

View File

@ -34,9 +34,9 @@ struct IdentifierNameRule: ASTRule, ConfigurationProviderRule {
}
return validateName(dictionary: dictionary, kind: kind).map { name, offset in
guard !configuration.excluded.contains(name), let firstCharacter = name.first else {
return []
}
guard let firstCharacter = name.first else { return [] }
guard !configuration.shouldExclude(name: name) else { return [] }
let isFunction = SwiftDeclarationKind.functionKinds.contains(kind)
let description = Self.description

View File

@ -2,6 +2,22 @@
import XCTest
class GenericTypeNameRuleTests: XCTestCase {
func testGenericTypeNameWithExcluded() {
let baseDescription = GenericTypeNameRule.description
let nonTriggeringExamples = baseDescription.nonTriggeringExamples + [
Example("func foo<apple> {}\n"),
Example("func foo<some_apple> {}\n"),
Example("func foo<test123> {}\n")
]
let triggeringExamples = baseDescription.triggeringExamples + [
Example("func foo<ap_ple> {}\n"),
Example("func foo<appleJuice> {}\n")
]
let description = baseDescription.with(nonTriggeringExamples: nonTriggeringExamples,
triggeringExamples: triggeringExamples)
verifyRule(description, ruleConfiguration: ["excluded": ["apple", "some.*", ".*st\\d+.*"]])
}
func testGenericTypeNameWithAllowedSymbols() {
let baseDescription = GenericTypeNameRule.description
let nonTriggeringExamples = baseDescription.nonTriggeringExamples + [

View File

@ -2,6 +2,22 @@
import XCTest
class IdentifierNameRuleTests: XCTestCase {
func testIdentifierNameWithExcluded() {
let baseDescription = IdentifierNameRule.description
let nonTriggeringExamples = baseDescription.nonTriggeringExamples + [
Example("let Apple = 0"),
Example("let some_apple = 0"),
Example("let Test123 = 0")
]
let triggeringExamples = baseDescription.triggeringExamples + [
Example("let ap_ple = 0"),
Example("let AppleJuice = 0")
]
let description = baseDescription.with(nonTriggeringExamples: nonTriggeringExamples,
triggeringExamples: triggeringExamples)
verifyRule(description, ruleConfiguration: ["excluded": ["Apple", "some.*", ".*\\d+.*"]])
}
func testIdentifierNameWithAllowedSymbols() {
let baseDescription = IdentifierNameRule.description
let nonTriggeringExamples = baseDescription.nonTriggeringExamples + [

View File

@ -2,6 +2,22 @@
import XCTest
class TypeNameRuleTests: XCTestCase {
func testTypeNameWithExcluded() {
let baseDescription = TypeNameRule.description
let nonTriggeringExamples = baseDescription.nonTriggeringExamples + [
Example("class apple {}"),
Example("struct some_apple {}"),
Example("protocol test123 {}")
]
let triggeringExamples = baseDescription.triggeringExamples + [
Example("enum ap_ple {}"),
Example("typealias appleJuice = Void")
]
let description = baseDescription.with(nonTriggeringExamples: nonTriggeringExamples,
triggeringExamples: triggeringExamples)
verifyRule(description, ruleConfiguration: ["excluded": ["apple", "some.*", ".*st\\d+.*"]])
}
func testTypeNameWithAllowedSymbols() {
let baseDescription = TypeNameRule.description
let nonTriggeringExamples = baseDescription.nonTriggeringExamples + [