Skip to content

Commit

Permalink
Record start positions of commands
Browse files Browse the repository at this point in the history
  • Loading branch information
SimplyDanny committed Jan 26, 2025
1 parent 7a129db commit f486bb3
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 117 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

* 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](https://github.com/SimplyDanny)
[#5945](https://github.com/realm/SwiftLint/issues/5945)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +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"),
Example("_ = \"🤵🏼‍♀️\" // swiftlint:disable:this unused_import", excludeFromDocumentation: true),
Example("_ = \"🤵🏼‍♀️ 🤵🏼‍♀️\" // swiftlint:disable:this unused_import", excludeFromDocumentation: true),
],
triggeringExamples: [
Example("// ↓swiftlint:"),
Expand All @@ -33,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 @@ -42,15 +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)?.unicodeScalars.first,
!CharacterSet.whitespaces.union(CharacterSet(charactersIn: "/")).contains(precedingCharacter) {
return styleViolation(
command.isPrecededByInvalidCharacter(in: file)
? styleViolation(
for: command,
in: file,
reason: "swiftlint command should be preceded by whitespace or a comment character"
)
}
return nil
: nil
}
}

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

private func styleViolation(for command: Command, in file: SwiftLintFile, reason: String) -> StyleViolation {
let character = command.startingCharacterPosition(in: file)
let characterOffset = character.flatMap {
if let line = command.lineOfCommand(in: file) {
return line.distance(from: line.startIndex, to: $0)
}
return nil
}
return StyleViolation(
StyleViolation(
ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file.path, line: command.line, character: characterOffset),
location: Location(file: file.path, line: command.line, character: command.character),
reason: reason
)
}
}

private extension Command {
func lineOfCommand(in file: SwiftLintFile) -> String? {
guard line > 0, line <= file.lines.count else {
return nil
}
return file.lines[line - 1].content
}

func startingCharacterPosition(in file: SwiftLintFile) -> String.Index? {
guard let line = lineOfCommand(in: file), line.isNotEmpty else {
return nil
}
if let commandIndex = line.range(of: "swiftlint:")?.lowerBound {
let distance = line.distance(from: line.startIndex, to: commandIndex)
return line.index(line.startIndex, offsetBy: distance + 1)
}
if let character {
return line.index(line.startIndex, offsetBy: character)
}
return nil
}

func precedingCharacter(in file: SwiftLintFile) -> Character? {
guard let startingCharacterPosition = startingCharacterPosition(in: file),
let line = lineOfCommand(in: file) else {
return nil
func isPrecededByInvalidCharacter(in file: SwiftLintFile) -> Bool {
guard line > 0, let character, character > 1, line <= file.lines.count else {
return false
}
guard line.distance(from: line.startIndex, to: startingCharacterPosition) > 2 else {
return nil
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 line[line.index(startingCharacterPosition, offsetBy: -2)...].first
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
}
}
8 changes: 3 additions & 5 deletions Source/SwiftLintCore/Models/Command.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ 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.
/// The character offset within the line in the source file where this command starts.
public let character: Int?
/// This command's modifier, if any.
public let modifier: Modifier?
Expand All @@ -63,8 +63,7 @@ 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 character: The character offset within the line in the source file where this command starts.
/// - parameter modifier: This command's modifier, if any.
/// - parameter trailingComment: The comment following this command's `-` delimiter, if any.
public init(action: Action,
Expand All @@ -85,8 +84,7 @@ 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.
/// - parameter character: The character offset within the line in the source file where this command starts.
public init(actionString: String, line: Int, character: Int) {
let scanner = Scanner(string: actionString)
_ = scanner.scanString("swiftlint:")
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,
character: character
)
commands.append(command)
default:
break
}
position += piece.sourceLength
}

return results
}
}
Loading

0 comments on commit f486bb3

Please sign in to comment.