Compare commits

...

5 Commits

Author SHA1 Message Date
Marcelo Fabri 805e761bee Change violation position for static/class decls 2022-11-06 01:01:55 -07:00
Marcelo Fabri cf7b3e03ad Only validate variable name for function param 2022-11-06 00:05:52 -07:00
Marcelo Fabri fc144dfe1f Catch for in 2022-11-06 00:05:52 -07:00
Marcelo Fabri 97bc3a3940 Catch function and closure params 2022-11-06 00:05:52 -07:00
Marcelo Fabri 8fea06f163 Rewrite `identifier_name` rule with SwiftSyntax 2022-11-06 00:05:52 -07:00
5 changed files with 146 additions and 94 deletions

View File

@ -112,6 +112,7 @@
- `generic_type_name` - `generic_type_name`
- `ibinspectable_in_extension` - `ibinspectable_in_extension`
- `identical_operands` - `identical_operands`
- `identifier_name`
- `implicit_getter` - `implicit_getter`
- `implicitly_unwrapped_optional` - `implicitly_unwrapped_optional`
- `inclusive_language` - `inclusive_language`

View File

@ -1,5 +1,5 @@
import Foundation import Foundation
import SourceKittenFramework import SwiftSyntax
extension String { extension String {
internal func hasTrailingWhitespace() -> Bool { internal func hasTrailingWhitespace() -> Bool {
@ -22,9 +22,8 @@ extension String {
return self == lowercased() return self == lowercased()
} }
internal func nameStrippingLeadingUnderscoreIfPrivate(_ dict: SourceKittenDictionary) -> String { internal func strippingLeadingUnderscoreIfPrivate(modifiers: ModifierListSyntax?) -> String {
if let acl = dict.accessibility, if first == "_", modifiers.isPrivateOrFileprivate {
acl.isPrivate && first == "_" {
return String(self[index(after: startIndex)...]) return String(self[index(after: startIndex)...])
} }
return self return self

View File

@ -133,13 +133,6 @@ private extension String {
return substring(from: 0, length: lastPreviewsIndex) return substring(from: 0, length: lastPreviewsIndex)
} }
func strippingLeadingUnderscoreIfPrivate(modifiers: ModifierListSyntax?) -> String {
if first == "_", modifiers.isPrivateOrFileprivate {
return String(self[index(after: startIndex)...])
}
return self
}
} }
private extension InheritedTypeListSyntax { private extension InheritedTypeListSyntax {

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
import SourceKittenFramework import SwiftSyntax
public struct IdentifierNameRule: ASTRule, ConfigurationProviderRule { public struct IdentifierNameRule: SwiftSyntaxRule, ConfigurationProviderRule {
public var configuration = NameConfiguration(minLengthWarning: 3, public var configuration = NameConfiguration(minLengthWarning: 3,
minLengthError: 2, minLengthError: 2,
maxLengthWarning: 40, maxLengthWarning: 40,
@ -24,107 +24,144 @@ public struct IdentifierNameRule: ASTRule, ConfigurationProviderRule {
deprecatedAliases: ["variable_name"] deprecatedAliases: ["variable_name"]
) )
// swiftlint:disable:next function_body_length public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
public func validate( Visitor(configuration: configuration)
file: SwiftLintFile, }
kind: SwiftDeclarationKind, }
dictionary: SourceKittenDictionary
) -> [StyleViolation] { private extension IdentifierNameRule {
guard !dictionary.enclosedSwiftAttributes.contains(.override) else { final class Visitor: ViolationsSyntaxVisitor {
return [] private let configuration: NameConfiguration
init(configuration: NameConfiguration) {
self.configuration = configuration
super.init(viewMode: .sourceAccurate)
} }
return validateName(dictionary: dictionary, kind: kind).map { name, offset in override func visitPost(_ node: FunctionDeclSyntax) {
guard !configuration.excluded.contains(name), let firstCharacter = name.first else { if let violation = violation(identifier: node.identifier, modifiers: node.modifiers, kind: .function,
return [] violationPosition: node.funcKeyword.positionAfterSkippingLeadingTrivia) {
violations.append(violation)
} }
}
let isFunction = SwiftDeclarationKind.functionKinds.contains(kind) override func visitPost(_ node: EnumCaseElementSyntax) {
let description = Self.description if let violation = violation(identifier: node.identifier, modifiers: nil, kind: .enumElement,
violationPosition: node.positionAfterSkippingLeadingTrivia) {
violations.append(violation)
}
}
let type = self.type(for: kind) override func visitPost(_ node: VariableDeclSyntax) {
if !isFunction { let violationPosition = node.letOrVarKeyword.positionAfterSkippingLeadingTrivia
let allowedSymbols = configuration.allowedSymbols.union(.alphanumerics) for binding in node.bindings {
if !allowedSymbols.isSuperset(of: CharacterSet(charactersIn: name)) { guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self),
return [ let violation = violation(identifier: pattern.identifier, modifiers: node.modifiers,
StyleViolation(ruleDescription: description, kind: .variable, violationPosition: violationPosition) else {
severity: .error, continue
location: Location(file: file, byteOffset: offset),
reason: "\(type) name should only contain alphanumeric " +
"characters: '\(name)'")
]
} }
if let severity = severity(forLength: name.count) { violations.append(violation)
let reason = "\(type) name should be between " + return
"\(configuration.minLengthThreshold) and " + }
"\(configuration.maxLengthThreshold) characters long: '\(name)'" }
return [
StyleViolation(ruleDescription: Self.description, override func visitPost(_ node: FunctionParameterSyntax) {
severity: severity, if let name = [node.secondName, node.firstName].compactMap({ $0 }).first,
location: Location(file: file, byteOffset: offset), let violation = violation(identifier: name, modifiers: node.modifiers, kind: .variable,
reason: reason) violationPosition: name.positionAfterSkippingLeadingTrivia) {
] violations.append(violation)
}
}
override func visitPost(_ node: ClosureParamSyntax) {
if let violation = violation(identifier: node.name, modifiers: nil, kind: .variable,
violationPosition: node.positionAfterSkippingLeadingTrivia) {
violations.append(violation)
}
}
override func visitPost(_ node: ForInStmtSyntax) {
if let pattern = node.pattern.as(IdentifierPatternSyntax.self),
let violation = violation(identifier: pattern.identifier, modifiers: nil, kind: .variable,
violationPosition: pattern.positionAfterSkippingLeadingTrivia) {
violations.append(violation)
}
}
private func violation(identifier: TokenSyntax,
modifiers: ModifierListSyntax?,
kind: ViolationKind,
violationPosition: AbsolutePosition) -> ReasonedRuleViolation? {
let name = identifier.text
.strippingLeadingUnderscoreIfPrivate(modifiers: modifiers)
.replacingOccurrences(of: "`", with: "")
guard name != "_",
!modifiers.containsOverride,
!configuration.excluded.contains(name),
let firstCharacter = name.first else {
return nil
}
let violationPosition = modifiers.staticOrClassPosition ?? violationPosition
if kind != .function {
let allowedSymbols = configuration.allowedSymbols.union(.alphanumerics)
if !allowedSymbols.isSuperset(of: CharacterSet(charactersIn: name)) {
return ReasonedRuleViolation(
position: violationPosition,
reason: "\(kind.stringValue) name should only contain alphanumeric characters: '\(name)'",
severity: .error
)
}
if let severity = configuration.severity(forLength: name.count) {
let reason = "\(kind.stringValue) name should be between " +
"\(configuration.minLengthThreshold) and " +
"\(configuration.maxLengthThreshold) characters long: '\(name)'"
return ReasonedRuleViolation(
position: violationPosition,
reason: reason,
severity: severity
)
} }
} }
let firstCharacterIsAllowed = configuration.allowedSymbols let firstCharacterIsAllowed = configuration.allowedSymbols
.isSuperset(of: CharacterSet(charactersIn: String(firstCharacter))) .isSuperset(of: CharacterSet(charactersIn: String(firstCharacter)))
guard !firstCharacterIsAllowed else { guard !firstCharacterIsAllowed else {
return [] return nil
} }
let requiresCaseCheck = configuration.validatesStartWithLowercase let requiresCaseCheck = configuration.validatesStartWithLowercase
if requiresCaseCheck && if requiresCaseCheck &&
kind != .varStatic && name.isViolatingCase && !name.isOperator { !modifiers.containsStaticOrClass && name.isViolatingCase && !name.isOperator {
let reason = "\(type) name should start with a lowercase character: '\(name)'" let reason = "\(kind.stringValue) name should start with a lowercase character: '\(name)'"
return [ return ReasonedRuleViolation(
StyleViolation(ruleDescription: description, position: violationPosition,
severity: .error, reason: reason,
location: Location(file: file, byteOffset: offset), severity: .error
reason: reason) )
]
} }
return [] return nil
} ?? []
}
private func validateName(
dictionary: SourceKittenDictionary,
kind: SwiftDeclarationKind
) -> (name: String, offset: ByteCount)? {
guard
var name = dictionary.name,
let offset = dictionary.offset,
kinds.contains(kind),
!name.hasPrefix("$")
else { return nil }
if
kind == .enumelement,
let parenIndex = name.firstIndex(of: "("),
parenIndex > name.startIndex
{
let index = name.index(before: parenIndex)
name = String(name[...index])
} }
return (name.nameStrippingLeadingUnderscoreIfPrivate(dictionary), offset)
} }
private let kinds: Set<SwiftDeclarationKind> = { enum ViolationKind {
return SwiftDeclarationKind.variableKinds case variable
.union(SwiftDeclarationKind.functionKinds) case function
.union([.enumelement]) case enumElement
}()
private func type(for kind: SwiftDeclarationKind) -> String { var stringValue: String {
if SwiftDeclarationKind.functionKinds.contains(kind) { switch self {
return "Function" case .variable:
} else if kind == .enumelement { return "Variable"
return "Enum element" case .function:
} else { return "Function"
return "Variable" case .enumElement:
return "Enum element"
}
} }
} }
} }
@ -147,3 +184,11 @@ private extension String {
return operators.contains(where: hasPrefix) return operators.contains(where: hasPrefix)
} }
} }
private extension ModifierListSyntax? {
var staticOrClassPosition: AbsolutePosition? {
self?.first { modifier in
modifier.name.tokenKind == .staticKeyword || modifier.name.tokenKind == .classKeyword
}?.positionAfterSkippingLeadingTrivia
}
}

View File

@ -13,7 +13,8 @@ internal struct IdentifierNameRuleExamples {
Example("func == (lhs: SyntaxToken, rhs: SyntaxToken) -> Bool"), Example("func == (lhs: SyntaxToken, rhs: SyntaxToken) -> Bool"),
Example("override func IsOperator(name: String) -> Bool"), Example("override func IsOperator(name: String) -> Bool"),
Example("enum Foo { case `private` }"), Example("enum Foo { case `private` }"),
Example("enum Foo { case value(String) }") Example("enum Foo { case value(String) }"),
Example("func group<U: Hashable>(by transform: (Element) -> U) -> [U: [Element]] {}")
] ]
static let triggeringExamples = [ static let triggeringExamples = [
@ -27,6 +28,19 @@ internal struct IdentifierNameRuleExamples {
Example("↓var aa = 0"), Example("↓var aa = 0"),
Example("private ↓let _i = 0"), Example("private ↓let _i = 0"),
Example("↓func IsOperator(name: String) -> Bool"), Example("↓func IsOperator(name: String) -> Bool"),
Example("enum Foo { case ↓MyEnum }") Example("func something(↓x: Int) -> Bool"),
Example("enum Foo { case ↓MyEnum }"),
Example("list.first { ↓l in l == 1 }"),
Example("for ↓i in 0..<10 {}"),
Example("""
class MyClass {
static var x: Int
}
"""),
Example("""
class MyClass {
class let i = 0
}
""")
] ]
} }