Files
SwiftLint/Source/SwiftLintFramework/Extensions/File+SwiftLint.swift
T
Norio Nomura 7c12a63e8f Merge commit '58eb0f69c4055bb2cb89b3df278eca6ce0fb1c34' into swift3.0
* commit '58eb0f69c4055bb2cb89b3df278eca6ce0fb1c34':
  generally clean up usage of swiftlint comment commands
  update README to reflect the ability to specify multiple rules in commands
  add changelog entry
  allow specifying multiple rule identifiers in comment commands

# Conflicts:
#	Source/SwiftLintFramework/Extensions/NSRegularExpression+SwiftLint.swift
#	Source/SwiftLintFramework/Models/Command.swift
#	Source/SwiftLintFramework/Rules/LegacyNSGeometryFunctionsRule.swift
#	Tests/SwiftLintFrameworkTests/ConfigurationTests.swift
#	Tests/SwiftLintFrameworkTests/IntegrationTests.swift
2016-11-30 18:42:40 +09:00

256 lines
9.8 KiB
Swift

//
// File+SwiftLint.swift
// SwiftLint
//
// Created by JP Simard on 2015-05-16.
// Copyright (c) 2015 Realm. All rights reserved.
//
import Foundation
import SourceKittenFramework
internal func regex(_ pattern: String) -> NSRegularExpression {
// all patterns used for regular expressions in SwiftLint are string literals which have been
// confirmed to work, so it's ok to force-try here.
// swiftlint:disable:next force_try
return try! .cached(pattern: pattern)
}
extension File {
internal func regions() -> [Region] {
var regions = [Region]()
var disabledRules = Set<String>()
let commands = self.commands()
let commandPairs = zip(commands, Array(commands.dropFirst().map(Optional.init)) + [nil])
for (command, nextCommand) in commandPairs {
switch command.action {
case .Disable: disabledRules.formUnion(command.ruleIdentifiers)
case .Enable: disabledRules.subtract(command.ruleIdentifiers)
}
let start = Location(file: path, line: command.line, character: command.character)
let end = endOfNextCommand(nextCommand)
regions.append(Region(start: start, end: end, disabledRuleIdentifiers: disabledRules))
}
return regions
}
fileprivate func commands() -> [Command] {
if sourcekitdFailed {
return []
}
let contents = self.contents as NSString
return matchPattern("swiftlint:(enable|disable)(:previous|:this|:next)?\\ [^\\s]+",
withSyntaxKinds: [.comment]).flatMap { range in
return Command(string: contents, range: range)
}.flatMap { command in
return command.expand()
}
}
fileprivate func endOfNextCommand(_ nextCommand: Command?) -> Location {
guard let nextCommand = nextCommand else {
return Location(file: path, line: Int.max, character: Int.max)
}
let nextLine: Int
let nextCharacter: Int?
if let nextCommandCharacter = nextCommand.character {
nextLine = nextCommand.line
if nextCommand.character! > 0 {
nextCharacter = nextCommandCharacter - 1
} else {
nextCharacter = nil
}
} else {
nextLine = max(nextCommand.line - 1, 0)
nextCharacter = Int.max
}
return Location(file: path, line: nextLine, character: nextCharacter)
}
internal func matchPattern(_ pattern: String,
withSyntaxKinds syntaxKinds: [SyntaxKind]) -> [NSRange] {
return matchPattern(pattern).filter { _, kindsInRange in
return kindsInRange.count == syntaxKinds.count &&
zip(kindsInRange, syntaxKinds).filter({ $0.0 != $0.1 }).isEmpty
}.map { $0.0 }
}
internal func rangesAndTokensMatching(_ pattern: String) -> [(NSRange, [SyntaxToken])] {
return rangesAndTokensMatching(regex(pattern))
}
internal func rangesAndTokensMatching(_ regex: NSRegularExpression) ->
[(NSRange, [SyntaxToken])] {
let contents = self.contents as NSString
let range = NSRange(location: 0, length: contents.length)
let syntax = syntaxMap
return regex.matches(in: self.contents, options: [], range: range).map { match in
let matchByteRange = contents.NSRangeToByteRange(start: match.range.location,
length: match.range.length) ?? match.range
let tokensInRange = syntax.tokensIn(matchByteRange)
return (match.range, tokensInRange)
}
}
internal func matchPattern(_ pattern: String) -> [(NSRange, [SyntaxKind])] {
return matchPattern(regex(pattern))
}
internal func matchPattern(_ regex: NSRegularExpression) -> [(NSRange, [SyntaxKind])] {
return rangesAndTokensMatching(regex).map { range, tokens in
(range, tokens.flatMap { SyntaxKind(rawValue: $0.type) })
}
}
internal func syntaxTokensByLine() -> [[SyntaxToken]]? {
if sourcekitdFailed {
return nil
}
var results = [[SyntaxToken]](repeating: [], count: lines.count + 1)
var tokenGenerator = syntaxMap.tokens.makeIterator()
var lineGenerator = lines.makeIterator()
var maybeLine = lineGenerator.next()
var maybeToken = tokenGenerator.next()
while let line = maybeLine, let token = maybeToken {
let tokenRange = NSRange(location: token.offset, length: token.length)
if NSLocationInRange(token.offset, line.byteRange) ||
NSLocationInRange(line.byteRange.location, tokenRange) {
results[line.index].append(token)
}
let tokenEnd = NSMaxRange(tokenRange)
let lineEnd = NSMaxRange(line.byteRange)
if tokenEnd < lineEnd {
maybeToken = tokenGenerator.next()
} else if tokenEnd > lineEnd {
maybeLine = lineGenerator.next()
} else {
maybeLine = lineGenerator.next()
maybeToken = tokenGenerator.next()
}
}
return results
}
internal func syntaxKindsByLine() -> [[SyntaxKind]]? {
if sourcekitdFailed {
return nil
}
guard let tokens = syntaxTokensByLine() else {
return nil
}
return tokens.map { $0.flatMap { SyntaxKind.init(rawValue: $0.type) } }
}
//Added by S2dent
/**
This function returns only matches that are not contained in a syntax kind
specified.
- parameter pattern: regex pattern to be matched inside file.
- parameter excludingSyntaxKinds: syntax kinds the matches to be filtered
when inside them.
- returns: An array of [NSRange] objects consisting of regex matches inside
file contents.
*/
internal func matchPattern(_ pattern: String,
excludingSyntaxKinds syntaxKinds: [SyntaxKind]) -> [NSRange] {
return matchPattern(pattern).filter {
$0.1.filter(syntaxKinds.contains).isEmpty
}.map { $0.0 }
}
internal func matchPattern(_ pattern: String,
excludingSyntaxKinds: [SyntaxKind],
excludingPattern: String) -> [NSRange] {
let contents = self.contents as NSString
let range = NSRange(location: 0, length: contents.length)
let matches = matchPattern(pattern, excludingSyntaxKinds: excludingSyntaxKinds)
if matches.isEmpty {
return []
}
let exclusionRanges = regex(excludingPattern).matches(in: self.contents,
options: [],
range: range)
.ranges()
return matches.filter { !$0.intersectsRanges(exclusionRanges) }
}
internal func validateVariableName(_ dictionary: [String: SourceKitRepresentable],
kind: SwiftDeclarationKind) -> (name: String, offset: Int)? {
guard let name = dictionary["key.name"] as? String,
let offset = (dictionary["key.offset"] as? Int64).flatMap({ Int($0) }) ,
SwiftDeclarationKind.variableKinds().contains(kind) && !name.hasPrefix("$") else {
return nil
}
return (name.nameStrippingLeadingUnderscoreIfPrivate(dictionary), offset)
}
internal func append(_ string: String) {
guard let stringData = string.data(using: .utf8) else {
fatalError("can't encode '\(string)' with UTF8")
}
guard let path = path, let fileHandle = FileHandle(forWritingAtPath: path) else {
fatalError("can't write to path '\(self.path)'")
}
fileHandle.seekToEndOfFile()
fileHandle.write(stringData)
fileHandle.closeFile()
contents += string
lines = contents.lines()
}
internal func write(_ string: String) {
guard string != contents else {
return
}
guard let path = path else {
fatalError("file needs a path to call write(_:)")
}
guard let stringData = string.data(using: .utf8) else {
fatalError("can't encode '\(string)' with UTF8")
}
do {
try stringData.write(to: URL(fileURLWithPath: path), options: .atomic)
} catch {
fatalError("can't write file to \(path)")
}
contents = string
lines = contents.lines()
}
internal func ruleEnabledViolatingRanges(_ violatingRanges: [NSRange],
forRule rule: Rule) -> [NSRange] {
let fileRegions = regions()
if fileRegions.isEmpty { return violatingRanges }
let violatingRanges = violatingRanges.filter { range in
let region = fileRegions.filter {
$0.contains(Location(file: self, characterOffset: range.location))
}.first
return region?.isRuleEnabled(rule) ?? true
}
return violatingRanges
}
fileprivate func numberOfCommentAndWhitespaceOnlyLines(_ startLine: Int, endLine: Int) -> Int {
let commentKinds = Set(SyntaxKind.commentKinds())
return syntaxKindsByLines[startLine...endLine].filter { kinds in
kinds.filter { !commentKinds.contains($0) }.isEmpty
}.count
}
internal func exceedsLineCountExcludingCommentsAndWhitespace(_ start: Int, _ end: Int,
_ limit: Int) -> (Bool, Int) {
if end - start <= limit {
return (false, end - start)
}
let count = end - start - numberOfCommentAndWhitespaceOnlyLines(start, endLine: end)
return (count > limit, count)
}
}