mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
234 lines
10 KiB
Swift
234 lines
10 KiB
Swift
//
|
|
// CodeOrganizationTests.swift
|
|
// SwiftFormatTests
|
|
//
|
|
// Created by Cal Stephens on 8/3/24.
|
|
// Copyright © 2024 Nick Lockwood. All rights reserved.
|
|
//
|
|
|
|
import XCTest
|
|
@testable import SwiftFormat
|
|
|
|
class CodeOrganizationTests: XCTestCase {
|
|
func testRuleFileCodeOrganization() throws {
|
|
for ruleFile in allRuleFiles {
|
|
let fileName = ruleFile.lastPathComponent
|
|
let titleCaseRuleName = fileName.replacingOccurrences(of: ".swift", with: "")
|
|
var ruleName = titleCaseRuleName.first!.lowercased() + titleCaseRuleName.dropFirst()
|
|
if titleCaseRuleName == "URLMacro" {
|
|
ruleName = "urlMacro"
|
|
}
|
|
|
|
let content = try String(contentsOf: ruleFile)
|
|
let formatter = Formatter(tokenize(content))
|
|
let declarations = formatter.parseDeclarations()
|
|
let extensions = declarations.filter { $0.keyword == "extension" }
|
|
|
|
for extensionDecl in extensions {
|
|
let extendedType = extensionDecl.name!
|
|
let extensionVisibility = extensionDecl.visibility() ?? .internal
|
|
|
|
if extendedType == "FormatRule" {
|
|
XCTAssertEqual(extensionVisibility, .public, """
|
|
Rule implementation in \(fileName) should be public.
|
|
""")
|
|
|
|
for bodyDeclaration in extensionDecl.body ?? [] {
|
|
XCTAssertEqual(bodyDeclaration.name, ruleName, """
|
|
A FormatRule named \(ruleName) should be the only declaration in \
|
|
the FormatRule extension in \(fileName).
|
|
""")
|
|
|
|
let declarationVisibility = bodyDeclaration.visibility() ?? extensionVisibility
|
|
XCTAssertEqual(declarationVisibility, .public, """
|
|
Rule implementation in \(fileName) should be public.
|
|
""")
|
|
}
|
|
continue
|
|
}
|
|
|
|
XCTAssertEqual(extensionVisibility, .internal, """
|
|
\(extendedType) extension in \(fileName) should be internal, \
|
|
to improve discoverability of helpers.
|
|
""")
|
|
|
|
for bodyDeclaration in extensionDecl.body ?? [] {
|
|
let declarationVisibility = bodyDeclaration.visibility() ?? extensionVisibility
|
|
XCTAssertEqual(declarationVisibility, .internal, """
|
|
\(bodyDeclaration.name!) helper in \(fileName) should be internal, \
|
|
to improve discoverability of helpers.
|
|
""")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func testRuleFileHelpersNotUsedByOtherRules() throws {
|
|
// Collect the name of all of the helpers defined in individual rule files
|
|
var allRuleFileHelpers: [(name: String, fileName: String, funcArgLabels: [String?]?)] = []
|
|
|
|
for ruleFile in allRuleFiles {
|
|
let fileName = ruleFile.lastPathComponent
|
|
let content = try String(contentsOf: ruleFile)
|
|
let formatter = Formatter(tokenize(content))
|
|
|
|
for declaration in formatter.parseDeclarations() {
|
|
guard declaration.keyword == "extension", let extendedType = declaration.name, extendedType != "FormatRule" else {
|
|
continue
|
|
}
|
|
|
|
for bodyDeclaration in declaration.body ?? [] {
|
|
guard let helperName = bodyDeclaration.name else { continue }
|
|
|
|
var helperFuncArgLabels: [String?]? = nil
|
|
if bodyDeclaration.keyword == "func", let startOfScope = formatter.index(of: .startOfScope("("), after: bodyDeclaration.range.lowerBound) {
|
|
helperFuncArgLabels = formatter.parseFunctionDeclarationArguments(startOfScope: startOfScope).map(\.externalLabel)
|
|
}
|
|
|
|
allRuleFileHelpers.append((name: helperName, fileName: fileName, funcArgLabels: helperFuncArgLabels))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify that none of the helpers defined in rule files are used in other files
|
|
let ruleFileHelperNames = Set(allRuleFileHelpers.map(\.name))
|
|
|
|
for file in allSourceFiles {
|
|
let fileName = file.lastPathComponent
|
|
let content = try String(contentsOf: file)
|
|
let formatter = Formatter(tokenize(content))
|
|
|
|
formatter.forEach(.identifier) { index, identifierToken in
|
|
let identifier = identifierToken.string
|
|
|
|
guard ruleFileHelperNames.contains(identifier) else { return }
|
|
|
|
// If this is a function call, parse the labels to disambiguate
|
|
// between methods with the same base name
|
|
var functionCallArguments: [String?]?
|
|
if let functionCallStartOfScope = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: index),
|
|
formatter.tokens[functionCallStartOfScope] == .startOfScope("(")
|
|
{
|
|
functionCallArguments = formatter.parseFunctionCallArguments(startOfScope: functionCallStartOfScope).map(\.label)
|
|
}
|
|
|
|
guard let matchingHelper = allRuleFileHelpers.first(where: { helper in
|
|
helper.name == identifier
|
|
&& helper.funcArgLabels == functionCallArguments
|
|
}), matchingHelper.fileName != fileName
|
|
else { return }
|
|
|
|
let fullHelperName: String
|
|
if let argumentLabels = matchingHelper.funcArgLabels {
|
|
let argumentLabelStrings = argumentLabels.map { label -> String in
|
|
if let label {
|
|
return label + ":"
|
|
} else {
|
|
return "_:"
|
|
}
|
|
}
|
|
|
|
fullHelperName = matchingHelper.name + "(" + argumentLabelStrings.joined() + ")"
|
|
} else {
|
|
fullHelperName = matchingHelper.name
|
|
}
|
|
|
|
XCTFail("""
|
|
\(fullHelperName) helper defined in \(matchingHelper.fileName) is also used in \(fileName). \
|
|
Shared helpers should be defined in a shared file like FormattingHelpers.swift,
|
|
ParsingHelpers.swift, or DeclarationHelpers.swift.
|
|
""")
|
|
}
|
|
}
|
|
}
|
|
|
|
func testRuleTestFilesHaveMatchingRule() {
|
|
let allRuleNames = Set(allRuleFiles.map { ruleFile -> String in
|
|
let fileName = ruleFile.lastPathComponent
|
|
let titleCaseRuleName = fileName.replacingOccurrences(of: ".swift", with: "")
|
|
var ruleName = titleCaseRuleName.first!.lowercased() + titleCaseRuleName.dropFirst()
|
|
if titleCaseRuleName == "URLMacro" {
|
|
ruleName = "urlMacro"
|
|
}
|
|
return ruleName
|
|
})
|
|
|
|
for testFile in allRuleTestFiles {
|
|
let testFileName = testFile.lastPathComponent
|
|
let expectedTestClassName = testFileName.replacingOccurrences(of: ".swift", with: "")
|
|
let titleCaseRuleName = expectedTestClassName.hasSuffix("Tests") ? String(expectedTestClassName.dropLast(5)) : expectedTestClassName
|
|
var ruleName = titleCaseRuleName.first!.lowercased() + titleCaseRuleName.dropFirst()
|
|
if titleCaseRuleName == "URLMacro" {
|
|
ruleName = "urlMacro"
|
|
}
|
|
|
|
XCTAssert(allRuleNames.contains(ruleName), """
|
|
\(testFileName) has no matching rule named \(ruleName).
|
|
""")
|
|
}
|
|
}
|
|
|
|
func testAllTestClassesMatchFileName() throws {
|
|
for testFile in allTestFiles {
|
|
let testFileName = testFile.lastPathComponent
|
|
let content = try String(contentsOf: testFile)
|
|
let formatter = Formatter(tokenize(content))
|
|
let declarations = formatter.parseDeclarations()
|
|
|
|
guard let testClass = declarations.first(where: { declaration in
|
|
let rangeBeforeKeyword = declaration.range.lowerBound ..< declaration.keywordIndex
|
|
return declaration.keyword == "class"
|
|
&& formatter.tokens[rangeBeforeKeyword].contains(.identifier("XCTestCase"))
|
|
}) else { continue }
|
|
|
|
let expectedTestClassName = testFileName.replacingOccurrences(of: ".swift", with: "")
|
|
|
|
XCTAssertEqual(testClass.name!, expectedTestClassName, """
|
|
class \(testClass.name!) and file \(testFileName) should have same name.
|
|
""")
|
|
}
|
|
}
|
|
|
|
func testTestCasesUseMultiLineStrings() throws {
|
|
for ruleTestFile in allRuleTestFiles {
|
|
let content = try String(contentsOf: ruleTestFile)
|
|
let formatter = Formatter(tokenize(content))
|
|
var hasChanges = false
|
|
|
|
formatter.forEach(.keyword) { index, keyword in
|
|
guard ["let", "var"].contains(keyword.string),
|
|
let propertyDeclaration = formatter.parsePropertyDeclaration(atIntroducerIndex: index),
|
|
let valueRange = propertyDeclaration.value?.expressionRange,
|
|
formatter.tokens[valueRange.lowerBound] == .startOfScope("\""),
|
|
let endOfString = formatter.endOfScope(at: valueRange.lowerBound)
|
|
else { return }
|
|
|
|
let startOfString = valueRange.lowerBound
|
|
let stringBodyRange = (startOfString + 1) ..< endOfString
|
|
|
|
let stringContent = formatter.tokens[stringBodyRange].map(\.string).joined()
|
|
let currentIndent = formatter.currentIndentForLine(at: startOfString)
|
|
let convertedContent = stringContent.replacingOccurrences(of: "\\n", with: "\n\(currentIndent)")
|
|
|
|
let newTokens: [Token] = [
|
|
.startOfScope("\"\"\""),
|
|
.linebreak("\n", 0),
|
|
.space(currentIndent),
|
|
.stringBody(convertedContent),
|
|
.linebreak("\n", 0),
|
|
.space(currentIndent),
|
|
.endOfScope("\"\"\""),
|
|
]
|
|
|
|
formatter.replaceTokens(in: startOfString ... endOfString, with: newTokens)
|
|
hasChanges = true
|
|
}
|
|
|
|
if hasChanges {
|
|
try formatter.tokens.string.write(to: ruleTestFile, atomically: true, encoding: .utf8)
|
|
XCTFail("Updated test cases in \(ruleTestFile.lastPathComponent) to use multi-line strings.")
|
|
}
|
|
}
|
|
}
|
|
}
|