Compare commits
5 Commits
main
...
marcelo/id
Author | SHA1 | Date |
---|---|---|
![]() |
805e761bee | |
![]() |
cf7b3e03ad | |
![]() |
fc144dfe1f | |
![]() |
97bc3a3940 | |
![]() |
8fea06f163 |
|
@ -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`
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
""")
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue