Files
SwiftLint/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift
2025-07-12 09:41:00 -04:00

114 lines
3.5 KiB
Swift

import SwiftSyntax
@SwiftSyntaxRule
struct OrphanedDocCommentRule: Rule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "orphaned_doc_comment",
name: "Orphaned Doc Comment",
description: "A doc comment should be attached to a declaration",
kind: .lint,
nonTriggeringExamples: [
Example("""
/// My great property
var myGreatProperty: String!
"""),
Example("""
//////////////////////////////////////
//
// Copyright header.
//
//////////////////////////////////////
"""),
Example("""
/// Look here for more info: https://github.com.
var myGreatProperty: String!
"""),
Example("""
/// Look here for more info:
/// https://github.com.
var myGreatProperty: String!
"""),
],
triggeringExamples: [
Example("""
↓/// My great property
// 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
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!
}
"""),
]
)
}
private extension OrphanedDocCommentRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
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
if !comment.hasPrefix("////"), !comment.hasPrefix("/***") {
if isOrphanedDocComment(with: &iterator) {
let utf8Length = pieces[..<index].reduce(0) { $0 + $1.sourceLength.utf8Length }
violations.append(node.position.advanced(by: utf8Length))
}
}
default:
break
}
}
}
}
}
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
}