mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
b83e0991b9
The MIT license doesn't require that all files be prepended with this licensing or copyright information. Realm confirmed that they're ok with this change. This will enable some companies to contribute to SwiftLint and the date & authorship information will remain accessible via git source control.
142 lines
6.3 KiB
Swift
142 lines
6.3 KiB
Swift
import Foundation
|
|
import SourceKittenFramework
|
|
|
|
public struct LineLengthRule: ConfigurationProviderRule {
|
|
public var configuration = LineLengthConfiguration(warning: 120, error: 200)
|
|
|
|
public init() {}
|
|
|
|
private let commentKinds = SyntaxKind.commentKinds
|
|
private let nonCommentKinds = SyntaxKind.allKinds.subtracting(SyntaxKind.commentKinds)
|
|
private let functionKinds = SwiftDeclarationKind.functionKinds
|
|
|
|
public static let description = RuleDescription(
|
|
identifier: "line_length",
|
|
name: "Line Length",
|
|
description: "Lines should not span too many characters.",
|
|
kind: .metrics,
|
|
nonTriggeringExamples: [
|
|
String(repeating: "/", count: 120) + "\n",
|
|
String(repeating: "#colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)", count: 120) + "\n",
|
|
String(repeating: "#imageLiteral(resourceName: \"image.jpg\")", count: 120) + "\n"
|
|
],
|
|
triggeringExamples: [
|
|
String(repeating: "/", count: 121) + "\n",
|
|
String(repeating: "#colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)", count: 121) + "\n",
|
|
String(repeating: "#imageLiteral(resourceName: \"image.jpg\")", count: 121) + "\n"
|
|
]
|
|
)
|
|
|
|
public func validate(file: File) -> [StyleViolation] {
|
|
let minValue = configuration.params.map({ $0.value }).min() ?? .max
|
|
let swiftDeclarationKindsByLine = file.swiftDeclarationKindsByLine() ?? []
|
|
let syntaxKindsByLine = file.syntaxKindsByLine() ?? []
|
|
|
|
return file.lines.compactMap { line in
|
|
// `line.content.count` <= `line.range.length` is true.
|
|
// So, `check line.range.length` is larger than minimum parameter value.
|
|
// for avoiding using heavy `line.content.count`.
|
|
if line.range.length < minValue {
|
|
return nil
|
|
}
|
|
|
|
if configuration.ignoresFunctionDeclarations &&
|
|
lineHasKinds(line: line,
|
|
kinds: functionKinds,
|
|
kindsByLine: swiftDeclarationKindsByLine) {
|
|
return nil
|
|
}
|
|
|
|
if configuration.ignoresComments &&
|
|
lineHasKinds(line: line,
|
|
kinds: commentKinds,
|
|
kindsByLine: syntaxKindsByLine) &&
|
|
!lineHasKinds(line: line,
|
|
kinds: nonCommentKinds,
|
|
kindsByLine: syntaxKindsByLine) {
|
|
return nil
|
|
}
|
|
|
|
var strippedString = line.content
|
|
if configuration.ignoresURLs {
|
|
strippedString = strippedString.strippingURLs
|
|
}
|
|
strippedString = stripLiterals(fromSourceString: strippedString,
|
|
withDelimiter: "#colorLiteral")
|
|
strippedString = stripLiterals(fromSourceString: strippedString,
|
|
withDelimiter: "#imageLiteral")
|
|
|
|
let length = strippedString.count
|
|
|
|
for param in configuration.params where length > param.value {
|
|
let reason = "Line should be \(configuration.length.warning) characters or less: " +
|
|
"currently \(length) characters"
|
|
return StyleViolation(ruleDescription: type(of: self).description,
|
|
severity: param.severity,
|
|
location: Location(file: file.path, line: line.index),
|
|
reason: reason)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Takes a string and replaces any literals specified by the `delimiter` parameter with `#`
|
|
///
|
|
/// - parameter sourceString: Original string, possibly containing literals
|
|
/// - parameter delimiter: Delimiter of the literal
|
|
/// (characters before the parentheses, e.g. `#colorLiteral`)
|
|
///
|
|
/// - returns: sourceString with the given literals replaced by `#`
|
|
private func stripLiterals(fromSourceString sourceString: String,
|
|
withDelimiter delimiter: String) -> String {
|
|
var modifiedString = sourceString
|
|
|
|
// While copy of content contains literal, replace with a single character
|
|
while modifiedString.contains("\(delimiter)(") {
|
|
if let rangeStart = modifiedString.range(of: "\(delimiter)("),
|
|
let rangeEnd = modifiedString.range(of: ")",
|
|
options: .literal,
|
|
range:
|
|
rangeStart.lowerBound..<modifiedString.endIndex) {
|
|
modifiedString.replaceSubrange(rangeStart.lowerBound..<rangeEnd.upperBound,
|
|
with: "#")
|
|
|
|
} else { // Should never be the case, but break to avoid accidental infinity loop
|
|
break
|
|
}
|
|
}
|
|
|
|
return modifiedString
|
|
}
|
|
|
|
private func lineHasKinds<Kind>(line: Line, kinds: Set<Kind>, kindsByLine: [[Kind]]) -> Bool {
|
|
let index = line.index
|
|
if index >= kindsByLine.count {
|
|
return false
|
|
}
|
|
return !kinds.isDisjoint(with: kindsByLine[index])
|
|
}
|
|
|
|
}
|
|
|
|
private extension String {
|
|
var strippingURLs: String {
|
|
let range = NSRange(location: 0, length: bridge().length)
|
|
// Workaround for Linux until NSDataDetector is available
|
|
#if os(Linux)
|
|
// Regex pattern from http://daringfireball.net/2010/07/improved_regex_for_matching_urls
|
|
let pattern = "(?i)\\b((?:[a-z][\\w-]+:(?:/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}/)" +
|
|
"(?:[^\\s()<>]+|\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\))+(?:\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*" +
|
|
"\\)|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))"
|
|
let urlRegex = regex(pattern)
|
|
return urlRegex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "")
|
|
#else
|
|
let types = NSTextCheckingResult.CheckingType.link.rawValue
|
|
guard let urlDetector = try? NSDataDetector(types: types) else {
|
|
return self
|
|
}
|
|
return urlDetector.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "")
|
|
#endif
|
|
}
|
|
}
|