Skip to content

Commit 2197103

Browse files
committed
Adjust PR to address comments I raised in PR feedback and address other comments found while addressing those
1 parent 52fdc83 commit 2197103

File tree

4 files changed

+593
-482
lines changed

4 files changed

+593
-482
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
extension Trivia {
14+
/// The contents of all the comment pieces with any comments markers removed and indentation whitespace stripped.
15+
public var commentValue: String? {
16+
var comments: [Substring] = []
17+
18+
/// Keep track of whether we have seen a line or block comment trivia piece. If this `Trivia` contains both a block
19+
/// and a line comment, we don't know how to concatenate them to form the comment value and thus default to
20+
/// returning `nil`.
21+
var hasBlockComment = false
22+
var hasLineComment = false
23+
24+
// Determine if all line comments have a space separating the `//` or `///` comment marker and the actual comment.
25+
lazy var allLineCommentsHaveSpace: Bool = pieces.allSatisfy { piece in
26+
switch piece {
27+
case .lineComment(let text): return text.hasPrefix("// ")
28+
case .docLineComment(let text): return text.hasPrefix("/// ")
29+
default: return true
30+
}
31+
}
32+
33+
// Strips /* */ markers and remove any common indentation between the lines in the block comment.
34+
func processBlockComment(_ text: String, isDocComment: Bool) -> String? {
35+
var lines = text.dropPrefix(isDocComment ? "/**" : "/*").dropSuffix("*/")
36+
.split(omittingEmptySubsequences: false, whereSeparator: \.isNewline)
37+
38+
// If the comment content starts on the same line as the `/*` marker or ends on the same line as the `*/` marker,
39+
// it is common to separate the marker and the actual comment using spaces. Strip those spaces if they exists.
40+
// If there are non no-space characters on the first / last line, then the comment doesn't start / end on the line
41+
// with the marker, so don't do the stripping.
42+
if let firstLine = lines.first, firstLine.contains(where: { $0 != " " }) {
43+
lines[0] = firstLine.drop { $0 == " " }
44+
}
45+
if let lastLine = lines.last, lastLine.contains(where: { $0 != " " }) {
46+
lines[lines.count - 1] = lastLine.dropLast { $0 == " " }
47+
}
48+
49+
var indentation: Substring? = nil
50+
// Find the lowest indentation that is common among all lines in the block comment. Do not consider the first line
51+
// because it won't have any indentation since it starts with /*
52+
for line in lines.dropFirst() {
53+
let lineIndentation = line.prefix(while: { $0 == " " || $0 == "\t" })
54+
guard let previousIndentation = indentation else {
55+
indentation = lineIndentation
56+
continue
57+
}
58+
indentation = commonPrefix(previousIndentation, lineIndentation)
59+
}
60+
61+
guard let firstLine = lines.first else {
62+
// We did not have any lines. This should never happen in practice because `split` never returns an empty array
63+
// but be safe and return `nil` here anyway.
64+
return nil
65+
}
66+
67+
var unindentedLines = [firstLine] + lines.dropFirst().map { $0.dropPrefix(indentation ?? "") }
68+
69+
// If the first line only contained the comment marker, don't include it. We don't want to start the comment value
70+
// with a newline if `/*` is on its own line. Same for the end marker.
71+
if unindentedLines.first?.allSatisfy({ $0 == " " }) ?? false {
72+
unindentedLines.removeFirst()
73+
}
74+
if unindentedLines.last?.allSatisfy({ $0 == " " }) ?? false {
75+
unindentedLines.removeLast()
76+
}
77+
// We canonicalize the line endings to `\n` here. This matches how we concatenate the different line comment
78+
// pieces using \n as well.
79+
return unindentedLines.joined(separator: "\n")
80+
}
81+
82+
for piece in pieces {
83+
switch piece {
84+
case .blockComment(let text), .docBlockComment(let text):
85+
if hasBlockComment || hasLineComment {
86+
return nil
87+
}
88+
hasBlockComment = true
89+
guard let processedText = processBlockComment(text, isDocComment: piece.isDocComment) else {
90+
return nil
91+
}
92+
comments.append(processedText[...])
93+
case .lineComment(let text), .docLineComment(let text):
94+
if hasBlockComment {
95+
return nil
96+
}
97+
hasLineComment = true
98+
let prefixToDrop = (piece.isDocComment ? "///" : "//") + (allLineCommentsHaveSpace ? " " : "")
99+
comments.append(text.dropPrefix(prefixToDrop))
100+
default:
101+
break
102+
}
103+
}
104+
105+
if comments.isEmpty { return nil }
106+
107+
return comments.joined(separator: "\n")
108+
}
109+
}
110+
111+
fileprivate extension StringProtocol where SubSequence == Substring {
112+
func dropPrefix(_ prefix: some StringProtocol) -> Substring {
113+
if self.hasPrefix(prefix) {
114+
return self.dropFirst(prefix.count)
115+
}
116+
return self[...]
117+
}
118+
119+
func dropSuffix(_ suffix: some StringProtocol) -> Substring {
120+
if self.hasSuffix(suffix) {
121+
return self.dropLast(suffix.count)
122+
}
123+
return self[...]
124+
}
125+
126+
func dropLast(while predicate: (Self.Element) -> Bool) -> Self.SubSequence {
127+
let dropLength = self.reversed().prefix(while: predicate)
128+
return self.dropLast(dropLength.count)
129+
}
130+
}
131+
132+
fileprivate func commonPrefix(_ lhs: Substring, _ rhs: Substring) -> Substring {
133+
return lhs[..<lhs.index(lhs.startIndex, offsetBy: zip(lhs, rhs).prefix { $0 == $1 }.count)]
134+
}
135+
136+
fileprivate extension TriviaPiece {
137+
var isDocComment: Bool {
138+
switch self {
139+
case .docBlockComment, .docLineComment: return true
140+
default: return false
141+
}
142+
}
143+
}

Sources/SwiftSyntax/Trivia.swift

Lines changed: 0 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -42,137 +42,6 @@ public struct Trivia: Sendable {
4242
pieces.isEmpty
4343
}
4444

45-
/// The string contents of all the comment pieces with any comments tokens trimmed.
46-
public var commentValue: String? {
47-
var comments = [Substring]()
48-
var hasBlockComment = false
49-
var hasLineComment = false
50-
51-
// Determine if all line comments have a single space
52-
lazy var allLineCommentsHaveSpace: Bool = {
53-
return pieces.allSatisfy { piece in
54-
switch piece {
55-
case .lineComment(let text):
56-
return text.hasPrefix("// ")
57-
case .docLineComment(let text):
58-
return text.hasPrefix("/// ")
59-
default:
60-
return true
61-
}
62-
}
63-
}()
64-
65-
// Returns a substring with leading and trailing spaces removed.
66-
func trimWhitespace(_ text: Substring) -> Substring {
67-
let trimmed = text.drop(while: { $0 == " " })
68-
let reversed = trimmed.reversed()
69-
let trimmedEnd = reversed.drop(while: { $0 == " " })
70-
let final = trimmedEnd.reversed()
71-
return Substring(final)
72-
}
73-
74-
// Strips /* */ markers and aligns content by removing common indentation.
75-
func processBlockComment(_ text: Substring) -> String {
76-
var lines = text.split(separator: "\n", omittingEmptySubsequences: false)
77-
78-
let (minSpaceIndentation, minTabIndentation) =
79-
lines
80-
.dropFirst()
81-
.filter { !$0.isEmpty }
82-
.reduce((Int.max, Int.max)) { (currentMin, line) in
83-
var spaceCount = 0, tabCount = 0
84-
var inLeadingWhitespace = true
85-
86-
for char in line {
87-
guard inLeadingWhitespace else { break }
88-
89-
switch char {
90-
case " ":
91-
spaceCount += 1
92-
case "\t":
93-
tabCount += 1
94-
default:
95-
inLeadingWhitespace = false
96-
}
97-
}
98-
99-
return (Swift.min(currentMin.0, spaceCount), Swift.min(currentMin.1, tabCount))
100-
}
101-
102-
var minIndentation = minSpaceIndentation == Int.max ? 0 : minSpaceIndentation
103-
minIndentation += minTabIndentation == Int.max ? 0 : minTabIndentation
104-
105-
if let first = lines.first {
106-
let prefixToDrop = first.hasPrefix("/**") ? 3 : 2
107-
lines[0] = first.dropFirst(prefixToDrop)
108-
}
109-
110-
var firstLineRemoved = false
111-
if trimWhitespace(lines[0]).isEmpty {
112-
lines.removeFirst()
113-
firstLineRemoved = true
114-
}
115-
116-
var unindentedLines = lines.enumerated().map { index, line -> Substring in
117-
if index == 0 && firstLineRemoved == false {
118-
return line
119-
}
120-
return line.count >= minIndentation ? line.dropFirst(minIndentation) : line
121-
}
122-
123-
if let last = unindentedLines.last, last.hasSuffix("*/") {
124-
unindentedLines[unindentedLines.count - 1] = last.dropLast(2)
125-
}
126-
127-
if trimWhitespace(unindentedLines[unindentedLines.count - 1]).isEmpty {
128-
unindentedLines.removeLast()
129-
}
130-
131-
return unindentedLines.joined(separator: "\n")
132-
}
133-
134-
for piece in pieces {
135-
switch piece {
136-
case .blockComment(let text), .docBlockComment(let text):
137-
if hasBlockComment || hasLineComment {
138-
return nil
139-
}
140-
hasBlockComment = true
141-
let processedText = processBlockComment(text[...])
142-
comments.append(processedText[...])
143-
144-
case .lineComment(let text):
145-
if hasBlockComment {
146-
return nil
147-
}
148-
hasLineComment = true
149-
let prefix = allLineCommentsHaveSpace ? "// " : "//"
150-
comments.append(text.dropFirst(prefix.count))
151-
152-
case .docLineComment(let text):
153-
if hasBlockComment {
154-
return nil
155-
}
156-
hasLineComment = true
157-
let prefix = allLineCommentsHaveSpace ? "/// " : "///"
158-
comments.append(text.dropFirst(prefix.count))
159-
160-
default:
161-
break
162-
}
163-
}
164-
165-
guard !comments.isEmpty else { return nil }
166-
167-
// If we have multiple line comments, they can be joined with newlines
168-
if hasLineComment {
169-
return comments.joined(separator: "\n")
170-
}
171-
172-
// In case of block comments, we should only have one
173-
return comments.first.map(String.init)
174-
}
175-
17645
/// The length of all the pieces in this ``Trivia``.
17746
public var sourceLength: SourceLength {
17847
return pieces.map({ $0.sourceLength }).reduce(.zero, +)

0 commit comments

Comments
 (0)