Files
SwiftLint/Source/SwiftLintFramework/Rules/LineLengthRule.swift
T
JP Simard b83e0991b9 Remove all file headers
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.
2018-05-04 13:42:02 -07:00

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
}
}