Skip to content

Commit f486bb3

Browse files
committed
Record start positions of commands
1 parent 7a129db commit f486bb3

File tree

8 files changed

+118
-117
lines changed

8 files changed

+118
-117
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
* Fix issue referencing the Tests package from another Bazel workspace.
2020
[jszumski](https://github.com/jszumski)
21+
2122
* Fix crash when a disable command is preceded by a unicode character.
2223
[SimplyDanny](https://github.com/SimplyDanny)
2324
[#5945](https://github.com/realm/SwiftLint/issues/5945)

Source/SwiftLintBuiltInRules/Rules/Lint/InvalidSwiftLintCommandRule.swift

Lines changed: 16 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ struct InvalidSwiftLintCommandRule: Rule, SourceKitFreeRule {
1515
Example("// swiftlint:disable:previous unused_import"),
1616
Example("// swiftlint:disable:this unused_import"),
1717
Example("//swiftlint:disable:this unused_import"),
18-
Example("_ = \"🤵🏼‍♀️\" // swiftlint:disable:this unused_import"),
18+
Example("_ = \"🤵🏼‍♀️\" // swiftlint:disable:this unused_import", excludeFromDocumentation: true),
19+
Example("_ = \"🤵🏼‍♀️ 🤵🏼‍♀️\" // swiftlint:disable:this unused_import", excludeFromDocumentation: true),
1920
],
2021
triggeringExamples: [
2122
Example("// ↓swiftlint:"),
@@ -33,6 +34,7 @@ struct InvalidSwiftLintCommandRule: Rule, SourceKitFreeRule {
3334
Example("// ↓swiftlint:enable: "),
3435
Example("// ↓swiftlint:disable: unused_import"),
3536
Example("// s↓swiftlint:disable unused_import"),
37+
Example("// 🤵🏼‍♀️swiftlint:disable unused_import", excludeFromDocumentation: true),
3638
].skipWrappingInCommentTests()
3739
)
3840

@@ -42,15 +44,13 @@ struct InvalidSwiftLintCommandRule: Rule, SourceKitFreeRule {
4244

4345
private func badPrefixViolations(in file: SwiftLintFile) -> [StyleViolation] {
4446
(file.commands + file.invalidCommands).compactMap { command in
45-
if let precedingCharacter = command.precedingCharacter(in: file)?.unicodeScalars.first,
46-
!CharacterSet.whitespaces.union(CharacterSet(charactersIn: "/")).contains(precedingCharacter) {
47-
return styleViolation(
47+
command.isPrecededByInvalidCharacter(in: file)
48+
? styleViolation(
4849
for: command,
4950
in: file,
5051
reason: "swiftlint command should be preceded by whitespace or a comment character"
5152
)
52-
}
53-
return nil
53+
: nil
5454
}
5555
}
5656

@@ -61,53 +61,26 @@ struct InvalidSwiftLintCommandRule: Rule, SourceKitFreeRule {
6161
}
6262

6363
private func styleViolation(for command: Command, in file: SwiftLintFile, reason: String) -> StyleViolation {
64-
let character = command.startingCharacterPosition(in: file)
65-
let characterOffset = character.flatMap {
66-
if let line = command.lineOfCommand(in: file) {
67-
return line.distance(from: line.startIndex, to: $0)
68-
}
69-
return nil
70-
}
71-
return StyleViolation(
64+
StyleViolation(
7265
ruleDescription: Self.description,
7366
severity: configuration.severity,
74-
location: Location(file: file.path, line: command.line, character: characterOffset),
67+
location: Location(file: file.path, line: command.line, character: command.character),
7568
reason: reason
7669
)
7770
}
7871
}
7972

8073
private extension Command {
81-
func lineOfCommand(in file: SwiftLintFile) -> String? {
82-
guard line > 0, line <= file.lines.count else {
83-
return nil
84-
}
85-
return file.lines[line - 1].content
86-
}
87-
88-
func startingCharacterPosition(in file: SwiftLintFile) -> String.Index? {
89-
guard let line = lineOfCommand(in: file), line.isNotEmpty else {
90-
return nil
91-
}
92-
if let commandIndex = line.range(of: "swiftlint:")?.lowerBound {
93-
let distance = line.distance(from: line.startIndex, to: commandIndex)
94-
return line.index(line.startIndex, offsetBy: distance + 1)
95-
}
96-
if let character {
97-
return line.index(line.startIndex, offsetBy: character)
98-
}
99-
return nil
100-
}
101-
102-
func precedingCharacter(in file: SwiftLintFile) -> Character? {
103-
guard let startingCharacterPosition = startingCharacterPosition(in: file),
104-
let line = lineOfCommand(in: file) else {
105-
return nil
74+
func isPrecededByInvalidCharacter(in file: SwiftLintFile) -> Bool {
75+
guard line > 0, let character, character > 1, line <= file.lines.count else {
76+
return false
10677
}
107-
guard line.distance(from: line.startIndex, to: startingCharacterPosition) > 2 else {
108-
return nil
78+
let line = file.lines[line - 1].content
79+
guard line.count > character,
80+
let char = line[line.index(line.startIndex, offsetBy: character - 2)].unicodeScalars.first else {
81+
return false
10982
}
110-
return line[line.index(startingCharacterPosition, offsetBy: -2)...].first
83+
return !CharacterSet.whitespaces.union(CharacterSet(charactersIn: "/")).contains(char)
11184
}
11285

11386
func invalidReason() -> String? {

Source/SwiftLintCore/Extensions/String+SwiftLint.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,17 @@ public extension String {
128128
func linesPrefixed(with prefix: Self) -> Self {
129129
split(separator: "\n").joined(separator: "\n\(prefix)")
130130
}
131+
132+
func characterPosition(of utf8Offset: Int) -> Int? {
133+
guard utf8Offset != 0 else {
134+
return 0
135+
}
136+
guard utf8Offset > 0, utf8Offset < lengthOfBytes(using: .utf8) else {
137+
return nil
138+
}
139+
for (offset, index) in indices.enumerated() where self[...index].lengthOfBytes(using: .utf8) == utf8Offset {
140+
return offset + 1
141+
}
142+
return nil
143+
}
131144
}

Source/SwiftLintCore/Models/Command.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public struct Command: Equatable {
5151
public let ruleIdentifiers: Set<RuleIdentifier>
5252
/// The line in the source file where this command is defined.
5353
public let line: Int
54-
/// The character offset within the line in the source file where this command is defined.
54+
/// The character offset within the line in the source file where this command starts.
5555
public let character: Int?
5656
/// This command's modifier, if any.
5757
public let modifier: Modifier?
@@ -63,8 +63,7 @@ public struct Command: Equatable {
6363
/// - parameter action: This command's action.
6464
/// - parameter ruleIdentifiers: The identifiers for the rules associated with this command.
6565
/// - parameter line: The line in the source file where this command is defined.
66-
/// - parameter character: The character offset within the line in the source file where this command is
67-
/// defined.
66+
/// - parameter character: The character offset within the line in the source file where this command starts.
6867
/// - parameter modifier: This command's modifier, if any.
6968
/// - parameter trailingComment: The comment following this command's `-` delimiter, if any.
7069
public init(action: Action,
@@ -85,8 +84,7 @@ public struct Command: Equatable {
8584
///
8685
/// - parameter actionString: The string in the command's definition describing its action.
8786
/// - parameter line: The line in the source file where this command is defined.
88-
/// - parameter character: The character offset within the line in the source file where this command is
89-
/// defined.
87+
/// - parameter character: The character offset within the line in the source file where this command starts.
9088
public init(actionString: String, line: Int, character: Int) {
9189
let scanner = Scanner(string: actionString)
9290
_ = scanner.scanString("swiftlint:")

Source/SwiftLintCore/Visitors/CommandVisitor.swift

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,37 +13,34 @@ final class CommandVisitor: SyntaxVisitor {
1313
}
1414

1515
override func visitPost(_ node: TokenSyntax) {
16-
let leadingCommands = node.leadingTrivia.commands(offset: node.position,
17-
locationConverter: locationConverter)
18-
let trailingCommands = node.trailingTrivia.commands(offset: node.endPositionBeforeTrailingTrivia,
19-
locationConverter: locationConverter)
20-
self.commands.append(contentsOf: leadingCommands + trailingCommands)
16+
collectCommands(in: node.leadingTrivia, offset: node.position)
17+
collectCommands(in: node.trailingTrivia, offset: node.endPositionBeforeTrailingTrivia)
2118
}
22-
}
23-
24-
// MARK: - Private Helpers
2519

26-
private extension Trivia {
27-
func commands(offset: AbsolutePosition, locationConverter: SourceLocationConverter) -> [Command] {
28-
var triviaOffset = SourceLength.zero
29-
var results: [Command] = []
30-
for trivia in self {
31-
triviaOffset += trivia.sourceLength
32-
switch trivia {
20+
private func collectCommands(in trivia: Trivia, offset: AbsolutePosition) {
21+
var position = offset
22+
for piece in trivia {
23+
switch piece {
3324
case .lineComment(let comment):
34-
guard let lower = comment.range(of: "swiftlint:")?.lowerBound else {
25+
guard let lower = comment.range(of: "swiftlint:")?.lowerBound.samePosition(in: comment.utf8) else {
3526
break
3627
}
37-
38-
let actionString = String(comment[lower...])
39-
let end = locationConverter.location(for: offset + triviaOffset)
40-
let command = Command(actionString: actionString, line: end.line, character: end.column)
41-
results.append(command)
28+
let offset = comment.utf8.distance(from: comment.utf8.startIndex, to: lower)
29+
let location = locationConverter.location(for: position.advanced(by: offset))
30+
let line = locationConverter.sourceLines[location.line - 1]
31+
guard let character = line.characterPosition(of: location.column) else {
32+
break
33+
}
34+
let command = Command(
35+
actionString: String(comment[lower...]),
36+
line: location.line,
37+
character: character
38+
)
39+
commands.append(command)
4240
default:
4341
break
4442
}
43+
position += piece.sourceLength
4544
}
46-
47-
return results
4845
}
4946
}

0 commit comments

Comments
 (0)