File: DocumentationCommentText.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (235 lines) | stat: -rw-r--r-- 8,228 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftSyntax

/// The text contents of a documentation comment extracted from trivia.
///
/// This type should be used when only the text of the comment is important, not the Markdown
/// structural organization. It automatically handles trimming leading indentation from comments as
/// well as "ASCII art" in block comments (i.e., leading asterisks on each line).
@_spi(Testing)
@_spi(Rules)
public struct DocumentationCommentText {
  /// Denotes the kind of punctuation used to introduce the comment.
  public enum Introducer {
    /// The comment was introduced entirely by line-style comments (`///`).
    case line

    /// The comment was introduced entirely by block-style comments (`/** ... */`).
    case block

    /// The comment was introduced by a mixture of line-style and block-style comments.
    case mixed
  }

  /// The comment text extracted from the trivia.
  public let text: String

  /// The index in the trivia collection passed to the initializer where the comment started.
  public let startIndex: Trivia.Index

  /// The kind of punctuation used to introduce the comment.
  public let introducer: Introducer

  /// Extracts and returns the body text of a documentation comment represented as a trivia
  /// collection.
  ///
  /// This implementation is based on
  /// https://github.com/apple/swift/blob/main/lib/Markup/LineList.cpp.
  ///
  /// - Parameter trivia: The trivia collection from which to extract the comment text.
  /// - Returns: If a comment was found, a tuple containing the `String` containing the extracted
  ///   text and the index into the trivia collection where the comment began is returned.
  ///   Otherwise, `nil` is returned.
  public init?(extractedFrom trivia: Trivia) {
    /// Represents a line of text and its leading indentation.
    struct Line {
      var text: Substring
      var firstNonspaceDistance: Int

      init(_ text: Substring) {
        self.text = text
        self.firstNonspaceDistance = indentationDistance(of: text)
      }
    }

    // Look backwards from the end of the trivia collection to find the logical start of the
    // comment. We have to copy it into an array since `Trivia` doesn't support bidirectional
    // indexing.
    let triviaArray = Array(trivia)
    let commentStartIndex = findCommentStartIndex(triviaArray)

    // Determine the indentation level of the first line of the comment. This is used to adjust
    // block comments, whose text spans multiple lines.
    let leadingWhitespace = contiguousWhitespace(in: triviaArray, before: commentStartIndex)
    var lines = [Line]()

    var introducer: Introducer?
    func updateIntroducer(_ newIntroducer: Introducer) {
      if let knownIntroducer = introducer, knownIntroducer != newIntroducer {
        introducer = .mixed
      } else {
        introducer = newIntroducer
      }
    }

    // Extract the raw lines of text (which will include their leading comment punctuation, which is
    // stripped).
    for triviaPiece in trivia[commentStartIndex...] {
      switch triviaPiece {
      case .docLineComment(let line):
        updateIntroducer(.line)
        lines.append(Line(line.dropFirst(3)))

      case .docBlockComment(let line):
        updateIntroducer(.block)

        var cleaned = line.dropFirst(3)
        if cleaned.hasSuffix("*/") {
          cleaned = cleaned.dropLast(2)
        }

        var hasASCIIArt = false
        if cleaned.hasPrefix("\n") {
          cleaned = cleaned.dropFirst()
          hasASCIIArt = asciiArtLength(of: cleaned, leadingSpaces: leadingWhitespace) != 0
        }

        while !cleaned.isEmpty {
          var index = cleaned.firstIndex(where: \.isNewline) ?? cleaned.endIndex
          if hasASCIIArt {
            cleaned =
              cleaned.dropFirst(asciiArtLength(of: cleaned, leadingSpaces: leadingWhitespace))
            index = cleaned.firstIndex(where: \.isNewline) ?? cleaned.endIndex
          }

          // Don't add an unnecessary blank line at the end when `*/` is on its own line.
          guard cleaned.firstIndex(where: { !$0.isWhitespace }) != nil else {
            break
          }

          let line = cleaned.prefix(upTo: index)
          lines.append(Line(line))
          cleaned = cleaned[index...].dropFirst()
        }

      default:
        break
      }
    }

    // Concatenate the lines into a single string, trimming any leading indentation that might be
    // present.
    guard
      let introducer = introducer,
      !lines.isEmpty,
      let firstLineIndex = lines.firstIndex(where: { !$0.text.isEmpty })
    else { return nil }

    let initialIndentation = indentationDistance(of: lines[firstLineIndex].text)
    var result = ""
    for line in lines[firstLineIndex...] {
      let countToDrop = min(initialIndentation, line.firstNonspaceDistance)
      result.append(contentsOf: "\(line.text.dropFirst(countToDrop))\n")
    }

    guard !result.isEmpty else { return nil }

    let commentStartDistance =
      triviaArray.distance(from: triviaArray.startIndex, to: commentStartIndex)
    self.text = result
    self.startIndex = trivia.index(trivia.startIndex, offsetBy: commentStartDistance)
    self.introducer = introducer
  }
}

/// Returns the distance from the start of the string to the first non-whitespace character.
private func indentationDistance(of text: Substring) -> Int {
  return text.distance(
    from: text.startIndex,
    to: text.firstIndex { !$0.isWhitespace } ?? text.endIndex)
}

/// Returns the number of contiguous whitespace characters (spaces and tabs only) that precede the
/// given trivia piece.
private func contiguousWhitespace(
  in trivia: [TriviaPiece],
  before index: Array<TriviaPiece>.Index
) -> Int {
  var index = index
  var whitespace = 0
  loop: while index != trivia.startIndex {
    index = trivia.index(before: index)
    switch trivia[index] {
    case .spaces(let count): whitespace += count
    case .tabs(let count): whitespace += count
    default: break loop
    }
  }
  return whitespace
}

/// Returns the number of characters considered block comment "ASCII art" at the beginning of the
/// given string.
private func asciiArtLength(of string: Substring, leadingSpaces: Int) -> Int {
  let spaces = string.prefix(leadingSpaces)
  if spaces.count != leadingSpaces {
    return 0
  }
  if spaces.contains(where: { !$0.isWhitespace }) {
    return 0
  }

  let string = string.dropFirst(leadingSpaces)
  if string.hasPrefix(" * ") {
    return leadingSpaces + 3
  }
  if string.hasPrefix(" *\n") {
    return leadingSpaces + 2
  }
  return 0
}

/// Returns the start index of the earliest comment in the Trivia if we work backwards and
/// skip through comments, newlines, and whitespace. Then we advance a bit forward to be sure
/// the returned index is actually a comment and not whitespace.
private func findCommentStartIndex(_ triviaArray: Array<TriviaPiece>) -> Array<TriviaPiece>.Index {
  func firstCommentIndex(_ slice: ArraySlice<TriviaPiece>) -> Array<TriviaPiece>.Index {
    return slice.firstIndex(where: {
      switch $0 {
      case .docLineComment, .docBlockComment:
        return true
      default:
        return false
      }
    }) ?? slice.endIndex
  }

  if
    let lastNonDocCommentIndex = triviaArray.lastIndex(where: {
      switch $0 {
      case .docBlockComment, .docLineComment,
          .newlines(1), .carriageReturns(1), .carriageReturnLineFeeds(1),
          .spaces, .tabs:
        return false
      default:
        return true
      }
    })
  {
    let nextIndex = triviaArray.index(after: lastNonDocCommentIndex)
    return firstCommentIndex(triviaArray[nextIndex...])
  } else {
    return firstCommentIndex(triviaArray[...])
  }
}