mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
328 lines
14 KiB
Swift
328 lines
14 KiB
Swift
//
|
|
// File+SwiftLint.swift
|
|
// SwiftLint
|
|
//
|
|
// Created by JP Simard on 2015-05-16.
|
|
// Copyright (c) 2015 Realm. All rights reserved.
|
|
//
|
|
|
|
import SourceKittenFramework
|
|
import SwiftXPC
|
|
|
|
typealias Line = (index: Int, content: String)
|
|
|
|
extension File {
|
|
func leadingWhitespaceViolations(contents: String) -> [StyleViolation] {
|
|
let countOfLeadingWhitespace = contents.countOfLeadingCharactersInSet(
|
|
NSCharacterSet.whitespaceAndNewlineCharacterSet()
|
|
)
|
|
if countOfLeadingWhitespace != 0 {
|
|
return [StyleViolation(type: .LeadingWhitespace,
|
|
location: Location(file: self.path, line: 1),
|
|
reason: "File shouldn't start with whitespace: " +
|
|
"currently starts with \(countOfLeadingWhitespace) whitespace characters")]
|
|
}
|
|
return []
|
|
}
|
|
|
|
func forceCastViolations() -> [StyleViolation] {
|
|
return matchPattern("as!", withSyntaxKinds: [.Keyword]).map { range in
|
|
return StyleViolation(type: .ForceCast,
|
|
location: Location(file: self, offset: range.location),
|
|
reason: "Force casts should be avoided")
|
|
}
|
|
}
|
|
|
|
func todoAndFixmeViolations() -> [StyleViolation] {
|
|
return matchPattern("// (TODO|FIXME):", withSyntaxKinds: [.Comment]).map { range in
|
|
return StyleViolation(type: .TODO,
|
|
location: Location(file: self, offset: range.location),
|
|
reason: "TODOs and FIXMEs should be avoided")
|
|
}
|
|
}
|
|
|
|
func colonViolations() -> [StyleViolation] {
|
|
let pattern1 = matchPattern("\\w+\\s+:\\s*\\S+",
|
|
withSyntaxKinds: [.Identifier, .Typeidentifier])
|
|
let pattern2 = matchPattern("\\w+:(?:\\s{0}|\\s{2,})\\S+",
|
|
withSyntaxKinds: [.Identifier, .Typeidentifier])
|
|
return (pattern1 + pattern2).map { range in
|
|
return StyleViolation(type: .Colon,
|
|
location: Location(file: self, offset: range.location),
|
|
reason: "When specifying a type, always associate the colon with the identifier")
|
|
}
|
|
}
|
|
|
|
func matchPattern(pattern: String, withSyntaxKinds syntaxKinds: [SyntaxKind] = []) -> [NSRange] {
|
|
return flatMap(NSRegularExpression(pattern: pattern, options: nil, error: nil)) { regex in
|
|
let range = NSRange(location: 0, length: count(self.contents.utf16))
|
|
let syntax = SyntaxMap(file: self)
|
|
let matches = regex.matchesInString(self.contents, options: nil, range: range)
|
|
return map(matches as? [NSTextCheckingResult]) { matches in
|
|
return compact(matches.map { match in
|
|
let tokensInRange = syntax.tokens.filter {
|
|
NSLocationInRange($0.offset, match.range)
|
|
}
|
|
let kindsInRange = compact(map(tokensInRange) {
|
|
SyntaxKind(rawValue: $0.type)
|
|
})
|
|
if kindsInRange.count != syntaxKinds.count {
|
|
return nil
|
|
}
|
|
for (index, kind) in enumerate(syntaxKinds) {
|
|
if kind != kindsInRange[index] {
|
|
return nil
|
|
}
|
|
}
|
|
return match.range
|
|
})
|
|
}
|
|
} ?? []
|
|
}
|
|
|
|
func trailingLineWhitespaceViolations(lines: [Line]) -> [StyleViolation] {
|
|
return lines.map { line in
|
|
(
|
|
index: line.index,
|
|
trailingWhitespaceCount: line.content.countOfTailingCharactersInSet(
|
|
NSCharacterSet.whitespaceCharacterSet()
|
|
)
|
|
)
|
|
}.filter {
|
|
$0.trailingWhitespaceCount > 0
|
|
}.map {
|
|
StyleViolation(type: .TrailingWhitespace,
|
|
location: Location(file: self.path, line: $0.index),
|
|
reason: "Line #\($0.index) should have no trailing whitespace: " +
|
|
"current has \($0.trailingWhitespaceCount) trailing whitespace characters")
|
|
}
|
|
}
|
|
|
|
func trailingNewlineViolations(contents: String) -> [StyleViolation] {
|
|
let countOfTrailingNewlines = contents.countOfTailingCharactersInSet(
|
|
NSCharacterSet.newlineCharacterSet()
|
|
)
|
|
if countOfTrailingNewlines != 1 {
|
|
return [StyleViolation(type: .TrailingNewline,
|
|
location: Location(file: self.path),
|
|
reason: "File should have a single trailing newline: " +
|
|
"currently has \(countOfTrailingNewlines)")]
|
|
}
|
|
return []
|
|
}
|
|
|
|
func fileLengthViolations(lines: [Line]) -> [StyleViolation] {
|
|
if lines.count > 400 {
|
|
return [StyleViolation(type: .Length,
|
|
location: Location(file: self.path),
|
|
reason: "File should contain 400 lines or less: currently contains \(lines.count)")]
|
|
}
|
|
return []
|
|
}
|
|
|
|
func astViolationsInDictionary(dictionary: XPCDictionary) -> [StyleViolation] {
|
|
return (dictionary["key.substructure"] as? XPCArray ?? []).flatMap {
|
|
// swiftlint:disable_rule:force_cast (safe to force cast)
|
|
let subDict = $0 as! XPCDictionary
|
|
// swiftlint:enable_rule:force_cast
|
|
var violations = self.astViolationsInDictionary(subDict)
|
|
if let kindString = subDict["key.kind"] as? String,
|
|
let kind = flatMap(kindString, { SwiftDeclarationKind(rawValue: $0) }) {
|
|
violations.extend(self.validateTypeName(kind, dict: subDict))
|
|
violations.extend(self.validateVariableName(kind, dict: subDict))
|
|
violations.extend(self.validateTypeBodyLength(kind, dict: subDict))
|
|
violations.extend(self.validateFunctionBodyLength(kind, dict: subDict))
|
|
violations.extend(self.validateNesting(kind, dict: subDict))
|
|
}
|
|
return violations
|
|
}
|
|
}
|
|
|
|
func validateTypeBodyLength(kind: SwiftDeclarationKind, dict: XPCDictionary) ->
|
|
[StyleViolation] {
|
|
let typeKinds: [SwiftDeclarationKind] = [
|
|
.Class,
|
|
.Struct,
|
|
.Enum
|
|
]
|
|
if !contains(typeKinds, kind) {
|
|
return []
|
|
}
|
|
var violations = [StyleViolation]()
|
|
if let offset = flatMap(dict["key.offset"] as? Int64, { Int($0) }),
|
|
let bodyOffset = flatMap(dict["key.bodyoffset"] as? Int64, { Int($0) }),
|
|
let bodyLength = flatMap(dict["key.bodylength"] as? Int64, { Int($0) }) {
|
|
let location = Location(file: self, offset: offset)
|
|
let startLine = self.contents.lineAndCharacterForByteOffset(bodyOffset)
|
|
let endLine = self.contents.lineAndCharacterForByteOffset(bodyOffset + bodyLength)
|
|
if let startLine = startLine?.line, let endLine = endLine?.line
|
|
where endLine - startLine > 200 {
|
|
violations.append(StyleViolation(type: .Length,
|
|
location: location,
|
|
reason: "Type body should be span 200 lines or less: currently spans " +
|
|
"\(endLine - startLine) lines"))
|
|
}
|
|
}
|
|
return violations
|
|
}
|
|
|
|
func validateFunctionBodyLength(kind: SwiftDeclarationKind, dict: XPCDictionary) ->
|
|
[StyleViolation] {
|
|
let functionKinds: [SwiftDeclarationKind] = [
|
|
.FunctionAccessorAddress,
|
|
.FunctionAccessorDidset,
|
|
.FunctionAccessorGetter,
|
|
.FunctionAccessorMutableaddress,
|
|
.FunctionAccessorSetter,
|
|
.FunctionAccessorWillset,
|
|
.FunctionConstructor,
|
|
.FunctionDestructor,
|
|
.FunctionFree,
|
|
.FunctionMethodClass,
|
|
.FunctionMethodInstance,
|
|
.FunctionMethodStatic,
|
|
.FunctionOperator,
|
|
.FunctionSubscript
|
|
]
|
|
if !contains(functionKinds, kind) {
|
|
return []
|
|
}
|
|
var violations = [StyleViolation]()
|
|
if let offset = flatMap(dict["key.offset"] as? Int64, { Int($0) }),
|
|
let bodyOffset = flatMap(dict["key.bodyoffset"] as? Int64, { Int($0) }),
|
|
let bodyLength = flatMap(dict["key.bodylength"] as? Int64, { Int($0) }) {
|
|
let location = Location(file: self, offset: offset)
|
|
let startLine = self.contents.lineAndCharacterForByteOffset(bodyOffset)
|
|
let endLine = self.contents.lineAndCharacterForByteOffset(bodyOffset + bodyLength)
|
|
if let startLine = startLine?.line, let endLine = endLine?.line
|
|
where endLine - startLine > 40 {
|
|
violations.append(StyleViolation(type: .Length,
|
|
location: location,
|
|
reason: "Function body should be span 40 lines or less: currently spans " +
|
|
"\(endLine - startLine) lines"))
|
|
}
|
|
}
|
|
return violations
|
|
}
|
|
|
|
func validateTypeName(kind: SwiftDeclarationKind, dict: XPCDictionary) -> [StyleViolation] {
|
|
let typeKinds: [SwiftDeclarationKind] = [
|
|
.Class,
|
|
.Struct,
|
|
.Typealias,
|
|
.Enum,
|
|
.Enumelement
|
|
]
|
|
if !contains(typeKinds, kind) {
|
|
return []
|
|
}
|
|
var violations = [StyleViolation]()
|
|
if let name = dict["key.name"] as? String,
|
|
let offset = flatMap(dict["key.offset"] as? Int64, { Int($0) }) {
|
|
let location = Location(file: self, offset: offset)
|
|
let nameCharacterSet = NSCharacterSet(charactersInString: name)
|
|
if !NSCharacterSet.alphanumericCharacterSet().isSupersetOfSet(nameCharacterSet) {
|
|
violations.append(StyleViolation(type: .NameFormat,
|
|
location: location,
|
|
reason: "Type name should only contain alphanumeric characters: '\(name)'"))
|
|
} else if !name.substringToIndex(name.startIndex.successor()).isUppercase() {
|
|
violations.append(StyleViolation(type: .NameFormat,
|
|
location: location,
|
|
reason: "Type name should start with an uppercase character: '\(name)'"))
|
|
} else if count(name) < 3 || count(name) > 40 {
|
|
violations.append(StyleViolation(type: .NameFormat,
|
|
location: location,
|
|
reason: "Type name should be between 3 and 40 characters in length: " +
|
|
"'\(name)'"))
|
|
}
|
|
}
|
|
return violations
|
|
}
|
|
|
|
func validateVariableName(kind: SwiftDeclarationKind, dict: XPCDictionary) -> [StyleViolation] {
|
|
let variableKinds: [SwiftDeclarationKind] = [
|
|
.VarClass,
|
|
.VarGlobal,
|
|
.VarInstance,
|
|
.VarLocal,
|
|
.VarParameter,
|
|
.VarStatic
|
|
]
|
|
if !contains(variableKinds, kind) {
|
|
return []
|
|
}
|
|
var violations = [StyleViolation]()
|
|
if let name = dict["key.name"] as? String,
|
|
let offset = flatMap(dict["key.offset"] as? Int64, { Int($0) }) {
|
|
let location = Location(file: self, offset: offset)
|
|
let nameCharacterSet = NSCharacterSet(charactersInString: name)
|
|
if !NSCharacterSet.alphanumericCharacterSet().isSupersetOfSet(nameCharacterSet) {
|
|
violations.append(StyleViolation(type: .NameFormat,
|
|
location: location,
|
|
reason: "Variable name should only contain alphanumeric characters: '\(name)'"))
|
|
} else if name.substringToIndex(name.startIndex.successor()).isUppercase() {
|
|
violations.append(StyleViolation(type: .NameFormat,
|
|
location: location,
|
|
reason: "Variable name should start with a lowercase character: '\(name)'"))
|
|
} else if count(name) < 3 || count(name) > 40 {
|
|
violations.append(StyleViolation(type: .NameFormat,
|
|
location: location,
|
|
reason: "Variable name should be between 3 and 40 characters in length: " +
|
|
"'\(name)'"))
|
|
}
|
|
}
|
|
return violations
|
|
}
|
|
|
|
func validateNesting(kind: SwiftDeclarationKind, dict: XPCDictionary, level: Int = 0) -> [StyleViolation] {
|
|
var violations = [StyleViolation]()
|
|
let typeKinds: [SwiftDeclarationKind] = [
|
|
.Class,
|
|
.Struct,
|
|
.Typealias,
|
|
.Enum,
|
|
.Enumelement
|
|
]
|
|
if let offset = flatMap(dict["key.offset"] as? Int64, { Int($0) }) {
|
|
if level > 1 && contains(typeKinds, kind) {
|
|
violations.append(StyleViolation(type: .Nesting,
|
|
location: Location(file: self, offset: offset),
|
|
reason: "Types should be nested at most 1 level deep"))
|
|
} else if level > 5 {
|
|
violations.append(StyleViolation(type: .Nesting,
|
|
location: Location(file: self, offset: offset),
|
|
reason: "Statements should be nested at most 5 levels deep"))
|
|
}
|
|
}
|
|
violations.extend(compact((dict["key.substructure"] as? XPCArray ?? []).map { subItem in
|
|
let subDict = subItem as? XPCDictionary
|
|
let kindString = subDict?["key.kind"] as? String
|
|
let kind = flatMap(kindString) { kindString in
|
|
return SwiftDeclarationKind(rawValue: kindString)
|
|
}
|
|
if let kind = kind, subDict = subDict {
|
|
return (kind, subDict)
|
|
}
|
|
return nil
|
|
} as [(SwiftDeclarationKind, XPCDictionary)?]).flatMap { (kind, dict) in
|
|
self.validateNesting(kind, dict: dict, level: level + 1)
|
|
})
|
|
return violations
|
|
}
|
|
|
|
internal var stringViolations: [StyleViolation] {
|
|
let lines = contents.lines()
|
|
// FIXME: Using '+' to concatenate these arrays would be nicer,
|
|
// but slows the compiler to a crawl.
|
|
var violations = LineLengthRule.validateFile(self)
|
|
violations.extend(leadingWhitespaceViolations(contents))
|
|
violations.extend(trailingLineWhitespaceViolations(lines))
|
|
violations.extend(trailingNewlineViolations(contents))
|
|
violations.extend(forceCastViolations())
|
|
violations.extend(fileLengthViolations(lines))
|
|
violations.extend(todoAndFixmeViolations())
|
|
violations.extend(colonViolations())
|
|
return violations
|
|
}
|
|
}
|