Skip to content

Commit cadc644

Browse files
committed
Update generate-doc-index to group all diagnostic groups together
Moves all the "diagnostic descriptions" into "diagnostic groups". This then allows some additional handling for: 1. Error when diagnostic files and their definition in `DiagnosticGroups.def` don't match up 2. Error when a title is missing its group name 3. List of all groups with warnings
1 parent c6e814f commit cadc644

File tree

1 file changed

+163
-113
lines changed

1 file changed

+163
-113
lines changed

utils/generate-doc-index.swift

Lines changed: 163 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -5,50 +5,26 @@ import Foundation
55
let usage = """
66
./\(CommandLine.arguments[0]) <swift-source-directory> [output-directory]
77
8-
Generates index files for diagnostics notes, groups, and upcoming features.
8+
Generates index files for diagnostics groups and upcoming features.
99
"""
1010

1111
let docsDir = "userdocs/diagnostics"
1212
let topLevelFileName = "diagnostics.md"
1313

14-
let notesDocFileName = "diagnostic-descriptions.md"
15-
let notesHeader = """
16-
# Diagnostic descriptions
17-
18-
<!-- This file is auto-generated via `swift swift/utils/generate-doc-index.swift` -->
19-
20-
Detailed explanations for various compiler diagnostics.
21-
22-
23-
## Overview
24-
25-
Swift diagnostics are classified into errors and warnings. Warnings can only be silenced in an
26-
intentional manner, e.g., adding `_ =` for an unused function result.
27-
28-
Some diagnostics have more detailed explanations available. These include a `[#Name]` inline and
29-
reference to this documentation at the end of the compiler output on the command line, or is
30-
presented specially within your IDE of choice. See below for the full list of these notes.
31-
32-
33-
## Topics
34-
35-
36-
"""
37-
3814
let groupsDocFileName = "diagnostic-groups.md"
3915
let groupsHeader = """
4016
# Diagnostic groups
4117
4218
<!-- This file is auto-generated via `swift swift/utils/generate-doc-index.swift` -->
4319
44-
Diagnostic groups allow controlling the behavior of warnings in a more precise manner.
20+
Detailed explanations for various compiler diagnostics.
4521
4622
4723
## Overview
4824
4925
Diagnostic groups collect some number of diagnostics together under a common group name. This allows
5026
for extra documentation to help explain relevant language concepts, as well as the ability to
51-
control the behavior of warnings in a more precise manner:
27+
control the behavior of warnings in a more precise manner (when that group contains warnings):
5228
- `-Werror <group>` - upgrades warnings in the specified group to errors
5329
- `-Wwarning <group>` - indicates that warnings in the specified group should remain warnings, even
5430
if they were previously upgraded to errors
@@ -62,11 +38,6 @@ Or upgrade all warnings except deprecated declaration to errors:
6238
```sh
6339
-warnings-as-errors -Wwarning DeprecatedDeclaration
6440
```
65-
66-
67-
## Topics
68-
69-
7041
"""
7142

7243
let featuresDocFileName = "upcoming-language-features.md"
@@ -88,18 +59,17 @@ enabled on the command line with `-enable-upcoming-feature <feature>`.
8859
Some upcoming features have an additional "migration" mode, where the compiler will emit warnings
8960
with fix-its to help migrate to that mode. This can be enabled with `-enable-upcoming-feature
9061
<feature>:migrate`.
62+
"""
9163

64+
let topicsHeader = "\n\n## Topics\n"
9265

93-
## Topics
94-
66+
let swiftIncludeDir = "include/swift"
9567

96-
"""
68+
let groupsFileName = "\(swiftIncludeDir)/AST/DiagnosticGroups.def"
69+
let groupRegex = /GROUP\((?<name>[a-zA-Z]+), "(?<file>.+)"\)/
9770

98-
let groupsFileName = "include/swift/AST/DiagnosticGroups.def"
99-
let groupRegex = /GROUP\(([a-zA-Z]+), ".+"\)/
100-
101-
let featuresFileName = "include/swift/Basic/Features.def"
102-
let featuresRegex = /UPCOMING_FEATURE\(([a-zA-Z]+), .+\)/
71+
let featuresFileName = "\(swiftIncludeDir)/Basic/Features.def"
72+
let featuresRegex = /UPCOMING_FEATURE\((?<name>[a-zA-Z]+), .+\)/
10373

10474
let nameRegex = /# .+ \((?<name>[a-zA-Z]+)\)/
10575

@@ -117,126 +87,206 @@ if !args.isEmpty {
11787
outputDir = "\(swiftSourceDir)/\(docsDir)"
11888
}
11989

120-
let generator = GenerateUserDocs(swiftSourceDir: swiftSourceDir, outputDir: outputDir)
12190
do {
122-
try generator.generateIndex()
91+
try generateIndex()
12392
} catch {
12493
print("error: \(error)")
12594
exit(1)
12695
}
12796

128-
struct GenerateUserDocs {
129-
let swiftSourceDir: String
130-
let outputDir: String
97+
func generateIndex() throws {
98+
let groupsHandle = try createIndex(name: groupsDocFileName, header: groupsHeader)
99+
defer { try? groupsHandle.close() }
100+
101+
let featuresHandle = try createIndex(name: featuresDocFileName, header: featuresHeader)
102+
defer { try? featuresHandle.close() }
131103

132-
func generateIndex() throws {
133-
let notesHandle = try createIndex(name: notesDocFileName, header: notesHeader)
134-
defer { try? notesHandle.close() }
104+
let groupsWithWarnings = try groupNamesWithWarnings()
105+
let docs = try retrieveDocs(groupsWithWarnings).sorted { a, b in
106+
return a.title < b.title
107+
}
135108

136-
let groupsHandle = try createIndex(name: groupsDocFileName, header: groupsHeader)
137-
defer { try? groupsHandle.close() }
109+
try groupsHandle.write(contentsOf: "\n\n## Groups with warnings\n".data(using: .utf8)!)
110+
for doc in docs where doc.kind == .groupWithWarnings {
111+
let ref = "- <doc:\(doc.name.dropLast(3))>\n"
112+
try groupsHandle.write(contentsOf: ref.data(using: .utf8)!)
113+
}
138114

139-
let featuresHandle = try createIndex(name: featuresDocFileName, header: featuresHeader)
140-
defer { try? featuresHandle.close() }
115+
try groupsHandle.write(contentsOf: topicsHeader.data(using: .utf8)!)
116+
try featuresHandle.write(contentsOf: topicsHeader.data(using: .utf8)!)
141117

142-
let docs = try retrieveDocs().sorted { a, b in
143-
return a.title < b.title
118+
for doc in docs {
119+
let handle: FileHandle
120+
switch doc.kind {
121+
case .group, .groupWithWarnings:
122+
handle = groupsHandle
123+
case .feature:
124+
handle = featuresHandle
144125
}
145126

146-
for doc in docs {
147-
let handle: FileHandle
148-
switch doc.kind {
149-
case .note:
150-
handle = notesHandle
151-
case .group:
152-
handle = groupsHandle
153-
case .feature:
154-
handle = featuresHandle
155-
}
127+
let ref = "- <doc:\(doc.name.dropLast(3))>\n"
128+
try handle.write(contentsOf: ref.data(using: .utf8)!)
129+
}
130+
}
156131

157-
let ref = "- <doc:\(doc.name.dropLast(3))>\n"
158-
try handle.write(contentsOf: ref.data(using: .utf8)!)
159-
}
132+
func createIndex(name: String, header: String) throws -> FileHandle {
133+
let path = "\(outputDir)/\(name)"
134+
135+
if FileManager.default.fileExists(atPath: path) {
136+
try FileManager.default.removeItem(atPath: path)
160137
}
138+
FileManager.default.createFile(atPath: path, contents: nil)
139+
140+
let handle = try FileHandle(forWritingTo: URL(filePath: path))
141+
try handle.write(contentsOf: header.data(using: .utf8)!)
142+
return handle
143+
}
161144

162-
func createIndex(name: String, header: String) throws -> FileHandle {
163-
let path = "\(outputDir)/\(name)"
145+
func retrieveDocs(_ groupsWithWarnings: Set<String>) throws -> [UserDoc] {
146+
let groups = Dictionary(try matches(in: "\(swiftSourceDir)/\(groupsFileName)", with: groupRegex) {
147+
(file: String($0.file), name: String($0.name))
148+
}, uniquingKeysWith: { a, b in a })
149+
let features = Set(try matches(in: "\(swiftSourceDir)/\(featuresFileName)", with: featuresRegex) {
150+
String($0.1)
151+
})
152+
153+
var docs: [UserDoc] = []
154+
155+
let files = try FileManager.default.contentsOfDirectory(atPath: "\(swiftSourceDir)/\(docsDir)")
156+
for name in files {
157+
if !name.hasSuffix(".md")
158+
|| name.hasSuffix(topLevelFileName)
159+
|| name.hasSuffix(groupsDocFileName)
160+
|| name.hasSuffix(featuresDocFileName) {
161+
continue
162+
}
163+
164+
guard let groupName = groups[String(name.dropLast(3))] else {
165+
throw GenerationError.unknownGroup(file: name)
166+
}
164167

165-
if FileManager.default.fileExists(atPath: path) {
166-
try FileManager.default.removeItem(atPath: path)
168+
let path = try String(contentsOfFile: "\(swiftSourceDir)/\(docsDir)/\(name)", encoding: .utf8)
169+
guard let match = try? nameRegex.prefixMatch(in: path) else {
170+
throw GenerationError.missingGroup(name: groupName, file: name)
167171
}
168-
FileManager.default.createFile(atPath: path, contents: nil)
169172

170-
let handle = try FileHandle(forWritingTo: URL(filePath: path))
171-
try handle.write(contentsOf: header.data(using: .utf8)!)
172-
return handle
173+
let titleGroupName = String(match.name)
174+
if groupName != titleGroupName {
175+
throw GenerationError.incorrectGroup(defsName: groupName, titleName: titleGroupName, file: name)
176+
}
177+
178+
let kind: UserDoc.Kind
179+
if features.contains(groupName) {
180+
kind = .feature
181+
} else if groupsWithWarnings.contains(groupName) {
182+
kind = .groupWithWarnings
183+
} else {
184+
kind = .group
185+
}
186+
187+
docs.append(UserDoc(name: name, title: String(match.0), kind: kind))
173188
}
174189

175-
func matches(in fileName: String, with regex: Regex<(Substring, Substring)>) throws -> Set<String> {
176-
let file = try String(contentsOfFile: "\(swiftSourceDir)/\(fileName)", encoding: .utf8)
190+
return docs
191+
}
177192

178-
var matches: Set<String> = []
179-
for line in file.components(separatedBy: .newlines) {
180-
if let match = try? regex.firstMatch(in: line) {
181-
matches.insert(String(match.1))
193+
func groupNamesWithWarnings() throws -> Set<String> {
194+
let includePath = "\(swiftSourceDir)/\(swiftIncludeDir)"
195+
let defPaths = try FileManager.default.subpathsOfDirectory(atPath: includePath)
196+
.compactMap { subpath in
197+
if subpath.hasSuffix(".def") {
198+
return "\(includePath)/\(subpath)"
182199
}
200+
return nil
183201
}
184202

185-
return matches
203+
enum WarningGroupState {
204+
case outside, inside, name
186205
}
187206

188-
func retrieveDocs() throws -> [UserDoc] {
189-
let groups = try matches(in: groupsFileName, with: groupRegex)
190-
let features = try matches(in: featuresFileName, with: featuresRegex)
207+
var groups: Set<String> = []
208+
for path in defPaths {
209+
let file = try String(contentsOfFile: path, encoding: .utf8)
191210

192-
var docs: [UserDoc] = []
211+
var state = WarningGroupState.outside
212+
for line in file.components(separatedBy: .newlines) {
213+
var line = Substring(line)
193214

194-
let files = try FileManager.default.contentsOfDirectory(atPath: "\(swiftSourceDir)/\(docsDir)")
195-
for name in files {
196-
if !name.hasSuffix(".md")
197-
|| name.hasSuffix(topLevelFileName)
198-
|| name.hasSuffix(notesDocFileName)
199-
|| name.hasSuffix(groupsDocFileName)
200-
|| name.hasSuffix(featuresDocFileName) {
201-
continue
202-
}
215+
switch state {
216+
case .outside:
217+
if !line.hasPrefix("GROUPED_WARNING") {
218+
continue
219+
}
220+
221+
state = .inside
222+
fallthrough
203223

204-
let file = try String(contentsOfFile: "\(swiftSourceDir)/\(docsDir)/\(name)", encoding: .utf8)
224+
case .inside:
225+
guard let index = line.firstIndex(of: ",") else {
226+
continue
227+
}
228+
line = line[index...].dropFirst()
205229

206-
if let match = try? nameRegex.prefixMatch(in: file) {
207-
let groupName = String(match.name)
230+
state = .name
231+
fallthrough
208232

209-
let kind: UserDoc.Kind
210-
if features.contains(groupName) {
211-
kind = .feature
212-
} else if groups.contains(groupName) {
213-
kind = .group
214-
} else {
215-
kind = .note
233+
case .name:
234+
if let index = line.firstIndex(of: ",") {
235+
line = line[..<index]
216236
}
237+
line = line.trimmingPrefix { $0.isWhitespace }
217238

218-
docs.append(UserDoc(name: name, title: String(match.0), kind: kind))
219-
} else {
220-
if let newlineIndex = file.firstIndex(of: "\n") {
221-
docs.append(UserDoc(name: name, title: String(file[..<newlineIndex]), kind: .note))
222-
} else {
223-
docs.append(UserDoc(name: name, title: file, kind: .note))
239+
if line.isEmpty {
240+
continue
224241
}
242+
243+
groups.insert(String(line))
244+
state = .outside
225245
}
226246
}
247+
}
248+
249+
return groups
250+
}
227251

228-
return docs
252+
func matches<R, E>(in path: String, with regex: Regex<R>, _ transform: (Regex<R>.Match) -> E) throws -> [E] {
253+
let file = try String(contentsOfFile: path, encoding: .utf8)
254+
255+
var matches = [E]()
256+
for line in file.components(separatedBy: .newlines) {
257+
if let match = try? regex.firstMatch(in: line) {
258+
matches.append(transform(match))
259+
}
229260
}
261+
262+
return matches
230263
}
231264

232265
struct UserDoc {
233266
enum Kind {
234-
case note
235267
case group
268+
case groupWithWarnings
236269
case feature
237270
}
238271

239272
let name: String
240273
let title: String
241274
let kind: Kind
242275
}
276+
277+
enum GenerationError: CustomStringConvertible, Error {
278+
case incorrectGroup(defsName: String, titleName: String, file: String)
279+
case missingGroup(name:String, file: String)
280+
case unknownGroup(file: String)
281+
282+
var description: String {
283+
switch self {
284+
case .incorrectGroup(let defsName, let titleName, let file):
285+
return "The title in '\(file)' contains the name '\(titleName)', but it should be '\(defsName)' as per 'DiagnosticGroups.def'"
286+
case .missingGroup(let name, let file):
287+
return "The title in '\(file)' does not end with a group name, add ' (\(name))' to the end of the title"
288+
case .unknownGroup(let file):
289+
return "'\(file)' has no corresponding listing in 'DiagnosticGroups.def'"
290+
}
291+
}
292+
}

0 commit comments

Comments
 (0)