Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix crash when disable command is preceded by unicode character #5976

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
* Fix issue referencing the Tests package from another Bazel workspace.
[jszumski](https://github.com/jszumski)

* Fix crash when a disable command is preceded by a unicode character.
SimplyDanny marked this conversation as resolved.
Show resolved Hide resolved
[SimplyDanny](https://github.com/SimplyDanny)
[#5945](https://github.com/realm/SwiftLint/issues/5945)

* Allow severity of `duplicate_imports` rule to be configurable.
[SimplyDanny](https://github.com/SimplyDanny)
[#5978](https://github.com/realm/SwiftLint/issues/5978)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ struct BlanketDisableCommandRule: Rule, SourceKitFreeRule {
Example("""
// swiftlint:enable ↓unused_import
"""),
Example("// swiftlint:disable all"),
].skipWrappingInCommentTests().skipDisableCommandTests()
)

Expand Down Expand Up @@ -189,7 +190,7 @@ struct BlanketDisableCommandRule: Rule, SourceKitFreeRule {

private extension Command {
func location(of ruleIdentifier: String, in file: SwiftLintFile) -> Location {
var location = character
var location = range?.upperBound
if line > 0, line <= file.lines.count {
let line = file.lines[line - 1].content
if let ruleIdentifierIndex = line.range(of: ruleIdentifier)?.lowerBound {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Foundation

struct InvalidSwiftLintCommandRule: Rule, SourceKitFreeRule {
var configuration = SeverityConfiguration<Self>(.warning)

Expand All @@ -13,6 +15,8 @@ struct InvalidSwiftLintCommandRule: Rule, SourceKitFreeRule {
Example("// swiftlint:disable:previous unused_import"),
Example("// swiftlint:disable:this unused_import"),
Example("//swiftlint:disable:this unused_import"),
Example("_ = \"🤵🏼‍♀️\" // swiftlint:disable:this unused_import", excludeFromDocumentation: true),
Example("_ = \"🤵🏼‍♀️ 🤵🏼‍♀️\" // swiftlint:disable:this unused_import", excludeFromDocumentation: true),
],
triggeringExamples: [
Example("// ↓swiftlint:"),
Expand All @@ -30,6 +34,7 @@ struct InvalidSwiftLintCommandRule: Rule, SourceKitFreeRule {
Example("// ↓swiftlint:enable: "),
Example("// ↓swiftlint:disable: unused_import"),
Example("// s↓swiftlint:disable unused_import"),
Example("// 🤵🏼‍♀️swiftlint:disable unused_import", excludeFromDocumentation: true),
].skipWrappingInCommentTests()
)

Expand All @@ -39,16 +44,13 @@ struct InvalidSwiftLintCommandRule: Rule, SourceKitFreeRule {

private func badPrefixViolations(in file: SwiftLintFile) -> [StyleViolation] {
(file.commands + file.invalidCommands).compactMap { command in
if let precedingCharacter = command.precedingCharacter(in: file)?.trimmingCharacters(in: .whitespaces) {
if !precedingCharacter.isEmpty, precedingCharacter != "/" {
return styleViolation(
for: command,
in: file,
reason: "swiftlint command should be preceded by whitespace or a comment character"
)
}
}
return nil
command.isPrecededByInvalidCharacter(in: file)
? styleViolation(
for: command,
in: file,
reason: "swiftlint command should be preceded by whitespace or a comment character"
)
: nil
}
}

Expand All @@ -59,34 +61,26 @@ struct InvalidSwiftLintCommandRule: Rule, SourceKitFreeRule {
}

private func styleViolation(for command: Command, in file: SwiftLintFile, reason: String) -> StyleViolation {
let character = command.startingCharacterPosition(in: file)
return StyleViolation(
StyleViolation(
ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file.path, line: command.line, character: character),
location: Location(file: file.path, line: command.line, character: command.range?.lowerBound),
reason: reason
)
}
}

private extension Command {
func startingCharacterPosition(in file: SwiftLintFile) -> Int? {
var position = character
if line > 0, line <= file.lines.count {
let line = file.lines[line - 1].content
if let commandIndex = line.range(of: "swiftlint:")?.lowerBound {
position = line.distance(from: line.startIndex, to: commandIndex) + 1
}
func isPrecededByInvalidCharacter(in file: SwiftLintFile) -> Bool {
guard line > 0, let character = range?.lowerBound, character > 1, line <= file.lines.count else {
return false
}
return position
}

func precedingCharacter(in file: SwiftLintFile) -> String? {
if let startingCharacterPosition = startingCharacterPosition(in: file), startingCharacterPosition > 2 {
let line = file.lines[line - 1].content
return line.substring(from: startingCharacterPosition - 2, length: 1)
let line = file.lines[line - 1].content
guard line.count > character,
let char = line[line.index(line.startIndex, offsetBy: character - 2)].unicodeScalars.first else {
return false
}
return nil
return !CharacterSet.whitespaces.union(CharacterSet(charactersIn: "/")).contains(char)
}

func invalidReason() -> String? {
Expand Down
13 changes: 13 additions & 0 deletions Source/SwiftLintCore/Extensions/String+SwiftLint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,17 @@ public extension String {
func linesPrefixed(with prefix: Self) -> Self {
split(separator: "\n").joined(separator: "\n\(prefix)")
}

func characterPosition(of utf8Offset: Int) -> Int? {
guard utf8Offset != 0 else {
return 0
}
guard utf8Offset > 0, utf8Offset < lengthOfBytes(using: .utf8) else {
return nil
}
for (offset, index) in indices.enumerated() where self[...index].lengthOfBytes(using: .utf8) == utf8Offset {
return offset + 1
}
return nil
}
}
6 changes: 3 additions & 3 deletions Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ extension SwiftLintFile {
break
}

let start = Location(file: path, line: command.line, character: command.character)
let start = Location(file: path, line: command.line, character: command.range?.upperBound)
let end = endOf(next: nextCommand)
guard start < end else { continue }
var didSetRegion = false
Expand Down Expand Up @@ -67,7 +67,7 @@ extension SwiftLintFile {
let rangeEnd = Location(file: self, characterOffset: NSMaxRange(range))
return commands
.filter { command in
let commandLocation = Location(file: path, line: command.line, character: command.character)
let commandLocation = Location(file: path, line: command.line, character: command.range?.upperBound)
return rangeStart <= commandLocation && commandLocation <= rangeEnd
}
.flatMap { $0.expand() }
Expand All @@ -79,7 +79,7 @@ extension SwiftLintFile {
}
let nextLine: Int
let nextCharacter: Int?
if let nextCommandCharacter = nextCommand.character {
if let nextCommandCharacter = nextCommand.range?.upperBound {
nextLine = nextCommand.line
if nextCommandCharacter > 0 {
nextCharacter = nextCommandCharacter - 1
Expand Down
28 changes: 13 additions & 15 deletions Source/SwiftLintCore/Models/Command.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ public struct Command: Equatable {
public let ruleIdentifiers: Set<RuleIdentifier>
/// The line in the source file where this command is defined.
public let line: Int
/// The character offset within the line in the source file where this command is defined.
public let character: Int?
/// The range of the command in the line (0-based).
public let range: Range<Int>?
/// This command's modifier, if any.
public let modifier: Modifier?
/// The comment following this command's `-` delimiter, if any.
Expand All @@ -63,20 +63,19 @@ public struct Command: Equatable {
/// - parameter action: This command's action.
/// - parameter ruleIdentifiers: The identifiers for the rules associated with this command.
/// - parameter line: The line in the source file where this command is defined.
/// - parameter character: The character offset within the line in the source file where this command is
/// defined.
/// - parameter range: The range of the command in the line (0-based).
/// - parameter modifier: This command's modifier, if any.
/// - parameter trailingComment: The comment following this command's `-` delimiter, if any.
public init(action: Action,
ruleIdentifiers: Set<RuleIdentifier> = [],
line: Int = 0,
character: Int? = nil,
range: Range<Int>? = nil,
modifier: Modifier? = nil,
trailingComment: String? = nil) {
self.action = action
self.ruleIdentifiers = ruleIdentifiers
self.line = line
self.character = character
self.range = range
self.modifier = modifier
self.trailingComment = trailingComment
}
Expand All @@ -85,21 +84,20 @@ public struct Command: Equatable {
///
/// - parameter actionString: The string in the command's definition describing its action.
/// - parameter line: The line in the source file where this command is defined.
/// - parameter character: The character offset within the line in the source file where this command is
/// defined.
public init(actionString: String, line: Int, character: Int) {
/// - parameter range: The range of the command in the line (0-based).
public init(actionString: String, line: Int, range: Range<Int>) {
SimplyDanny marked this conversation as resolved.
Show resolved Hide resolved
let scanner = Scanner(string: actionString)
_ = scanner.scanString("swiftlint:")
// (enable|disable)(:previous|:this|:next)
guard let actionAndModifierString = scanner.scanUpToString(" ") else {
self.init(action: .invalid, line: line, character: character)
self.init(action: .invalid, line: line, range: range)
return
}
let actionAndModifierScanner = Scanner(string: actionAndModifierString)
guard let actionString = actionAndModifierScanner.scanUpToString(":"),
let action = Action(rawValue: actionString)
else {
self.init(action: .invalid, line: line, character: character)
self.init(action: .invalid, line: line, range: range)
return
}

Expand Down Expand Up @@ -137,7 +135,7 @@ public struct Command: Equatable {
action: action,
ruleIdentifiers: ruleIdentifiers,
line: line,
character: character,
range: range,
modifier: modifier,
trailingComment: trailingComment
)
Expand All @@ -155,17 +153,17 @@ public struct Command: Equatable {
case .previous:
return [
Self(action: action, ruleIdentifiers: ruleIdentifiers, line: line - 1),
Self(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line - 1, character: Int.max),
Self(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line - 1, range: 0..<Int.max),
]
case .this:
return [
Self(action: action, ruleIdentifiers: ruleIdentifiers, line: line),
Self(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line, character: Int.max),
Self(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line, range: 0..<Int.max),
]
case .next:
return [
Self(action: action, ruleIdentifiers: ruleIdentifiers, line: line + 1),
Self(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line + 1, character: Int.max),
Self(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line + 1, range: 0..<Int.max),
]
case .invalid:
return []
Expand Down
43 changes: 20 additions & 23 deletions Source/SwiftLintCore/Visitors/CommandVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,34 @@ final class CommandVisitor: SyntaxVisitor {
}

override func visitPost(_ node: TokenSyntax) {
let leadingCommands = node.leadingTrivia.commands(offset: node.position,
locationConverter: locationConverter)
let trailingCommands = node.trailingTrivia.commands(offset: node.endPositionBeforeTrailingTrivia,
locationConverter: locationConverter)
self.commands.append(contentsOf: leadingCommands + trailingCommands)
collectCommands(in: node.leadingTrivia, offset: node.position)
collectCommands(in: node.trailingTrivia, offset: node.endPositionBeforeTrailingTrivia)
}
}

// MARK: - Private Helpers

private extension Trivia {
func commands(offset: AbsolutePosition, locationConverter: SourceLocationConverter) -> [Command] {
var triviaOffset = SourceLength.zero
var results: [Command] = []
for trivia in self {
triviaOffset += trivia.sourceLength
switch trivia {
private func collectCommands(in trivia: Trivia, offset: AbsolutePosition) {
var position = offset
for piece in trivia {
switch piece {
case .lineComment(let comment):
guard let lower = comment.range(of: "swiftlint:")?.lowerBound else {
guard let lower = comment.range(of: "swiftlint:")?.lowerBound.samePosition(in: comment.utf8) else {
break
}

let actionString = String(comment[lower...])
let end = locationConverter.location(for: offset + triviaOffset)
let command = Command(actionString: actionString, line: end.line, character: end.column)
results.append(command)
let offset = comment.utf8.distance(from: comment.utf8.startIndex, to: lower)
let location = locationConverter.location(for: position.advanced(by: offset))
let line = locationConverter.sourceLines[location.line - 1]
guard let character = line.characterPosition(of: location.column) else {
break
}
let command = Command(
actionString: String(comment[lower...]),
line: location.line,
range: character..<(character + piece.sourceLength.utf8Length - offset)
)
commands.append(command)
default:
break
}
position += piece.sourceLength
}

return results
}
}
Loading