112 lines
4.1 KiB
Swift
112 lines
4.1 KiB
Swift
import Foundation
|
|
import SourceKittenFramework
|
|
|
|
struct FileHeaderRule: ConfigurationProviderRule, OptInRule {
|
|
var configuration = FileHeaderConfiguration()
|
|
|
|
init() {}
|
|
|
|
static let description = RuleDescription(
|
|
identifier: "file_header",
|
|
name: "File Header",
|
|
description: "Header comments should be consistent with project patterns. " +
|
|
"The SWIFTLINT_CURRENT_FILENAME placeholder can optionally be used in the " +
|
|
"required and forbidden patterns. It will be replaced by the real file name.",
|
|
kind: .style,
|
|
nonTriggeringExamples: [
|
|
Example("let foo = \"Copyright\""),
|
|
Example("let foo = 2 // Copyright"),
|
|
Example("let foo = 2\n // Copyright")
|
|
],
|
|
triggeringExamples: [
|
|
Example("// ↓Copyright\n"),
|
|
Example("//\n// ↓Copyright"),
|
|
Example("""
|
|
//
|
|
// FileHeaderRule.swift
|
|
// SwiftLint
|
|
//
|
|
// Created by Marcelo Fabri on 27/11/16.
|
|
// ↓Copyright © 2016 Realm. All rights reserved.
|
|
//
|
|
""")
|
|
].skipWrappingInCommentTests()
|
|
)
|
|
|
|
func validate(file: SwiftLintFile) -> [StyleViolation] {
|
|
var firstToken: SwiftLintSyntaxToken?
|
|
var lastToken: SwiftLintSyntaxToken?
|
|
var firstNonCommentToken: SwiftLintSyntaxToken?
|
|
|
|
for token in file.syntaxTokensByLines.lazy.joined() {
|
|
guard let kind = token.kind, kind.isFileHeaderKind else {
|
|
// found a token that is not a comment, which means it's not the top of the file
|
|
// so we can just skip the remaining tokens
|
|
firstNonCommentToken = token
|
|
break
|
|
}
|
|
|
|
// skip SwiftLint commands
|
|
guard !isSwiftLintCommand(token: token, file: file) else {
|
|
continue
|
|
}
|
|
|
|
if firstToken == nil {
|
|
firstToken = token
|
|
}
|
|
lastToken = token
|
|
}
|
|
|
|
let requiredRegex = configuration.requiredRegex(for: file)
|
|
|
|
var violationsOffsets = [Int]()
|
|
if let firstToken, let lastToken {
|
|
let start = firstToken.offset
|
|
let length = lastToken.offset + lastToken.length - firstToken.offset
|
|
let byteRange = ByteRange(location: start, length: length)
|
|
guard let range = file.stringView.byteRangeToNSRange(byteRange) else {
|
|
return []
|
|
}
|
|
|
|
if let regex = configuration.forbiddenRegex(for: file),
|
|
let firstMatch = regex.matches(in: file.contents, options: [], range: range).first {
|
|
violationsOffsets.append(firstMatch.range.location)
|
|
}
|
|
|
|
if let regex = requiredRegex,
|
|
case let matches = regex.matches(in: file.contents, options: [], range: range),
|
|
matches.isEmpty {
|
|
violationsOffsets.append(file.stringView.location(fromByteOffset: start))
|
|
}
|
|
} else if requiredRegex != nil {
|
|
let location = firstNonCommentToken.map {
|
|
Location(file: file, byteOffset: $0.offset)
|
|
} ?? Location(file: file.path, line: 1)
|
|
return [makeViolation(at: location)]
|
|
}
|
|
|
|
return violationsOffsets.map { makeViolation(at: Location(file: file, characterOffset: $0)) }
|
|
}
|
|
|
|
private func isSwiftLintCommand(token: SwiftLintSyntaxToken, file: SwiftLintFile) -> Bool {
|
|
guard let range = file.stringView.byteRangeToNSRange(token.range) else {
|
|
return false
|
|
}
|
|
|
|
return file.commands(in: range).isNotEmpty
|
|
}
|
|
|
|
private func makeViolation(at location: Location) -> StyleViolation {
|
|
return StyleViolation(ruleDescription: Self.description,
|
|
severity: configuration.severityConfiguration.severity,
|
|
location: location,
|
|
reason: "Header comments should be consistent with project patterns")
|
|
}
|
|
}
|
|
|
|
private extension SyntaxKind {
|
|
var isFileHeaderKind: Bool {
|
|
return self == .comment || self == .commentURL
|
|
}
|
|
}
|