Files
SwiftLint/Source/SwiftLintFramework/Rules/TodoRule.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

89 lines
3.0 KiB
Swift

import Foundation
import SourceKittenFramework
public extension SyntaxKind {
/// Returns if the syntax kind is comment-like.
var isCommentLike: Bool {
return SyntaxKind.commentKinds.contains(self)
}
}
public struct TodoRule: ConfigurationProviderRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "todo",
name: "Todo",
description: "TODOs and FIXMEs should be resolved.",
kind: .lint,
nonTriggeringExamples: [
"// notaTODO:\n",
"// notaFIXME:\n"
],
triggeringExamples: [
"// ↓TODO:\n",
"// ↓FIXME:\n",
"// ↓TODO(note)\n",
"// ↓FIXME(note)\n",
"/* ↓FIXME: */\n",
"/* ↓TODO: */\n",
"/** ↓FIXME: */\n",
"/** ↓TODO: */\n"
]
)
private func customMessage(file: File, range: NSRange) -> String {
var reason = type(of: self).description.description
let offset = NSMaxRange(range)
guard let (lineNumber, _) = file.contents.bridge().lineAndCharacter(forCharacterOffset: offset) else {
return reason
}
let line = file.lines[lineNumber - 1]
// customizing the reason message to be specific to fixme or todo
let violationSubstring = file.contents.bridge().substring(with: range)
let range = NSRange(location: offset, length: NSMaxRange(line.range) - offset)
var message = file.contents.bridge().substring(with: range)
let kind = violationSubstring.hasPrefix("FIXME") ? "FIXMEs" : "TODOs"
// trim whitespace
message = message.trimmingCharacters(in: .whitespacesAndNewlines)
// limiting the output length of todo message
let maxLengthOfMessage = 30
if message.utf16.count > maxLengthOfMessage {
let index = message.index(message.startIndex,
offsetBy: maxLengthOfMessage,
limitedBy: message.endIndex) ?? message.endIndex
message = message[..<index] + "..."
}
if message.isEmpty {
reason = "\(kind) should be resolved."
} else {
reason = "\(kind) should be resolved (\(message))."
}
return reason
}
public func validate(file: File) -> [StyleViolation] {
return file.match(pattern: "\\b(?:TODO|FIXME)(?::|\\b)").compactMap { range, syntaxKinds in
if !syntaxKinds.filter({ !$0.isCommentLike }).isEmpty {
return nil
}
let reason = customMessage(file: file, range: range)
return StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, characterOffset: range.location),
reason: reason)
}
}
}