Skip to content

Commit a09e768

Browse files
committed
Add infrastructure for automated validation of syntax node structure
The goal here is to generalize the API inconsistency reported by @AnthonyLatsis and automatically detect them. There are valid reasons for nodes or children to violate these base rules. My idea is that we note these exceptions in expected failures, ideally with a sort explanation that motivates the deviation from standard naming rules. We aren’t running these tests at the moment yet. I’d like to merge them as a starting point for API improvement ideas at the moment. The current rules are - All nodes with base kind e.g. `ExprSyntax` should end with `ExprSyntax`. - If a child has a single keyword as its choice, it should be named `*Keyword` (e.g. `ImportKeyword`) - If a child has a single non-keyword token kind, name it the same as the the token choice (e.g. `LeftParen`) - If a token only has keyword token choices, its name should end with `Keyword`. - Children that have the same type also have the same child name.
1 parent a90e1be commit a09e768

File tree

6 files changed

+632
-0
lines changed

6 files changed

+632
-0
lines changed

CodeGeneration/Package.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ let package = Package(
4545
"SyntaxSupport",
4646
]
4747
),
48+
.testTarget(
49+
name: "ValidateSyntaxNodes",
50+
dependencies: [
51+
"SyntaxSupport"
52+
]
53+
),
4854
]
4955
)
5056

CodeGeneration/Tests/ValidateSyntaxNodes/ExpectedFailures.swift

Lines changed: 380 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 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 Collection {
14+
/// If the collection contains a single element, return it, otherwise `nil`.
15+
var only: Element? {
16+
if !isEmpty && index(after: startIndex) == endIndex {
17+
return self.first!
18+
} else {
19+
return nil
20+
}
21+
}
22+
}
23+
24+
extension String {
25+
func dropSuffix(_ suffix: String) -> String {
26+
if hasSuffix(suffix) {
27+
return String(self.dropLast(suffix.count))
28+
} else {
29+
return self
30+
}
31+
}
32+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 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+
import SyntaxSupport
14+
15+
import XCTest
16+
17+
fileprivate func assertNoFailures(
18+
_ failures: [ValidationFailure],
19+
message: String,
20+
file: StaticString = #file,
21+
line: UInt = #line
22+
) {
23+
if failures.isEmpty {
24+
return
25+
}
26+
var message = "\(message): \n"
27+
let failuresByNode = [SyntaxNodeKind: [ValidationFailure]](failures.map({ ($0.node, [$0]) })) { $0 + $1 }
28+
29+
for (nodeKind, failures) in failuresByNode.sorted(by: { $0.key.rawValue < $1.key.rawValue }) {
30+
message += "\(nodeKind.syntaxType):\n"
31+
for failure in failures {
32+
message += " - \(failure.message)\n"
33+
}
34+
}
35+
XCTFail(message, file: file, line: line)
36+
}
37+
38+
class ValidateSyntaxNodes: XCTestCase {
39+
func testValidation() {
40+
var failures: [ValidationFailure] = []
41+
failures += validateBaseKindSuffix()
42+
failures += validateSingleTokenChoiceChildNaming()
43+
failures += validateMultipleKeywordChoicesNaming()
44+
failures += validateConsistentNamingOfChildren()
45+
46+
let matchedXFails = expectedFailures.filter { failures.contains($0) }
47+
failures = failures.filter { !matchedXFails.contains($0) }
48+
let unmatchedXFails = expectedFailures.filter { !matchedXFails.contains($0) }
49+
50+
assertNoFailures(failures, message: "Unexpected valiation failures found")
51+
assertNoFailures(unmatchedXFails, message: "Unmatched expected failures")
52+
53+
for failure in failures {
54+
print("\(failure),")
55+
}
56+
}
57+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 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+
import SyntaxSupport
14+
15+
/// All nodes with base kind e.g. `ExprSyntax` should end with `ExprSyntax`.
16+
func validateBaseKindSuffix() -> [ValidationFailure] {
17+
var failures: [ValidationFailure] = []
18+
for node in SYNTAX_NODES where node.base != .syntaxCollection {
19+
if !node.kind.syntaxType.description.hasSuffix(node.base.syntaxType.description) {
20+
failures.append(
21+
ValidationFailure(node: node.kind, message: "has base kind '\(node.base.syntaxType)' but type name doesn’t have '\(node.base.syntaxType)' suffix")
22+
)
23+
}
24+
}
25+
return failures
26+
}
27+
28+
/// Checks standardized naming of children with a single token choice
29+
///
30+
/// - If a child has a single keyword as its choice, it should be named `*Keyword` (e.g. `ImportKeyword`)
31+
/// - If it’s another token kind, name it the same as the the token choice (e.g. `LeftParen`)
32+
func validateSingleTokenChoiceChildNaming() -> [ValidationFailure] {
33+
var failures: [ValidationFailure] = []
34+
for node in SYNTAX_NODES.compactMap(\.layoutNode) {
35+
for child in node.children {
36+
if case .token(choices: let tokenChoices, _, _) = child.kind,
37+
let choice = tokenChoices.only
38+
{
39+
switch choice {
40+
case .keyword(text: let keyword):
41+
if child.name != "\(keyword.withFirstCharacterUppercased)Keyword" {
42+
failures.append(
43+
ValidationFailure(
44+
node: node.kind,
45+
message:
46+
"child '\(child.name)' has a single keyword as its only token choice and should thus be named '\(keyword.withFirstCharacterUppercased)Keyword'"
47+
)
48+
)
49+
}
50+
case .token(tokenKind: let tokenKind):
51+
switch tokenKind {
52+
case "IdentifierToken":
53+
// We allow arbitrary naming of identifiers
54+
break
55+
case "CommaToken":
56+
if child.name != "TrailingComma" && child.name != "Comma" {
57+
failures.append(
58+
ValidationFailure(
59+
node: node.kind,
60+
message: "child '\(child.name)' has a comma keyword as its only token choice and should thus be named 'Comma' or 'TrailingComma'"
61+
)
62+
)
63+
}
64+
default:
65+
if child.name != tokenKind.dropSuffix("Token") {
66+
failures.append(
67+
ValidationFailure(
68+
node: node.kind,
69+
message: "child '\(child.name)' has a token as its only token choice and should thus be named '\(tokenKind.dropSuffix("Token"))'"
70+
)
71+
)
72+
}
73+
}
74+
}
75+
}
76+
}
77+
}
78+
return failures
79+
}
80+
81+
/// If a token only has keyword token choices, its name should end with `Keyword`.
82+
func validateMultipleKeywordChoicesNaming() -> [ValidationFailure] {
83+
var failures: [ValidationFailure] = []
84+
for node in SYNTAX_NODES.compactMap(\.layoutNode) {
85+
for child in node.children {
86+
if case .token(choices: let tokenChoices, _, _) = child.kind,
87+
tokenChoices.count > 1, // single token choices are handled by `validateSingleTokenChoiceChildNaming`
88+
tokenChoices.allSatisfy({ $0.isKeyword }),
89+
!child.name.hasSuffix("Keyword")
90+
{
91+
failures.append(
92+
ValidationFailure(
93+
node: node.kind,
94+
message: "child '\(child.name)' only has keywords as its token choices and should thus and with 'Keyword'"
95+
)
96+
)
97+
}
98+
}
99+
}
100+
return failures
101+
}
102+
103+
/// Check that children that have the same type also have the same child name.
104+
func validateConsistentNamingOfChildren() -> [ValidationFailure] {
105+
var failures: [ValidationFailure] = []
106+
107+
var childrenByNodeKind: [SyntaxNodeKind: [(node: LayoutNode, child: Child)]] = [:]
108+
109+
for node in SYNTAX_NODES.compactMap(\.layoutNode) {
110+
for child in node.children {
111+
guard case .node(kind: let childKind) = child.kind else {
112+
continue
113+
}
114+
childrenByNodeKind[childKind, default: []].append((node, child))
115+
}
116+
}
117+
118+
for (kind, children) in childrenByNodeKind where !kind.isBase && kind != .token {
119+
let childNames = children.map(\.child.name)
120+
let firstChildName = childNames.first!
121+
122+
for (node, child) in children.dropFirst() {
123+
if child.name != firstChildName {
124+
failures.append(
125+
ValidationFailure(
126+
node: node.kind,
127+
message:
128+
"child '\(child.name)' is named inconsistently with '\(children.first!.node.kind.syntaxType).\(children.first!.child.name)', which has the same type ('\(kind.syntaxType)')"
129+
)
130+
)
131+
}
132+
}
133+
}
134+
135+
return failures
136+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 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+
import SyntaxSupport
14+
15+
struct ValidationFailure: Equatable {
16+
/// The node that failed verification
17+
let node: SyntaxNodeKind
18+
19+
/// A human-readable description of the validation failure
20+
let message: String
21+
}

0 commit comments

Comments
 (0)