From 2be8846ad326cb3f4bacdae5ca3956844661a4c1 Mon Sep 17 00:00:00 2001 From: Hayashi Tatsuya <62803132+swiftty@users.noreply.github.com> Date: Sun, 21 Jan 2024 17:20:12 +0900 Subject: [PATCH] Rewrite `orphaned_doc_comment` rule with SwiftSyntax (#5417) --- CHANGELOG.md | 1 + .../Rules/Lint/OrphanedDocCommentRule.swift | 84 ++++++++++++++----- 2 files changed, 62 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2698d4a683..0a74c9f0b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ * `multiline_literal_brackets` * `nimble_operator` * `opening_brace` + * `orphaned_doc_comment` * `trailing_closure` * `void_return` diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift index c721621dbd..acd440a5ae 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift @@ -1,6 +1,7 @@ -import SwiftIDEUtils +import SwiftSyntax -struct OrphanedDocCommentRule: SourceKitFreeRule { +@SwiftSyntaxRule +struct OrphanedDocCommentRule: Rule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( @@ -40,36 +41,73 @@ struct OrphanedDocCommentRule: SourceKitFreeRule { ↓/// Look here for more info: https://github.com. // Not a doc string var myGreatProperty: String! + """), + Example(""" + ↓/// Look here for more info: https://github.com. + + + // Not a doc string + var myGreatProperty: String! + """), + Example(""" + ↓/// Look here for more info: https://github.com. + // Not a doc string + ↓/// My great property + // Not a doc string + var myGreatProperty: String! + """), + Example(""" + extension Nested { + ↓/// + /// Look here for more info: https://github.com. + + // Not a doc string + var myGreatProperty: String! + } """) ] ) +} - func validate(file: SwiftLintFile) -> [StyleViolation] { - file.syntaxClassifications - .filter { $0.kind != .none } - .pairs() - .compactMap { first, second in - let firstByteRange = first.range.toSourceKittenByteRange() - guard - let second, - first.kind == .docLineComment || first.kind == .docBlockComment, - second.kind == .lineComment || second.kind == .blockComment, - let firstString = file.stringView.substringWithByteRange(firstByteRange), +private extension OrphanedDocCommentRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: TokenSyntax) { + let pieces = node.leadingTrivia.pieces + var iterator = pieces.enumerated().makeIterator() + while let (index, piece) = iterator.next() { + switch piece { + case .docLineComment(let comment), .docBlockComment(let comment): // These patterns are often used for "file header" style comments - !firstString.starts(with: "////") && !firstString.starts(with: "/***") - else { - return nil - } + if !comment.hasPrefix("////") && !comment.hasPrefix("/***") { + if isOrphanedDocComment(with: &iterator) { + let utf8Length = pieces[.. Zip2Sequence { - return zip(self, Array(dropFirst()) + [nil]) +private func isOrphanedDocComment( + with iterator: inout some IteratorProtocol<(offset: Int, element: TriviaPiece)> +) -> Bool { + while let (_, piece) = iterator.next() { + switch piece { + case .docLineComment, .docBlockComment, + .carriageReturns, .carriageReturnLineFeeds, .newlines, .spaces: + break + + case .lineComment, .blockComment: + return true + + default: + return false + } } + return false }