// // FileHeaderTests.swift // SwiftFormatTests // // Created by Nick Lockwood on 3/7/17. // Copyright © 2024 Nick Lockwood. All rights reserved. // import XCTest @testable import SwiftFormat final class FileHeaderTests: XCTestCase { func testStripHeader() { let input = """ // // test.swift // SwiftFormat // // Created by Nick Lockwood on 08/11/2016. // Copyright © 2016 Nick Lockwood. All rights reserved. // /// func func foo() {} """ let output = """ /// func func foo() {} """ let options = FormatOptions(fileHeader: "") testFormatting(for: input, output, rule: .fileHeader, options: options) } func testStripHeaderWithWhenHeaderContainsUrl() { let input = """ // // RulesTests+General.swift // SwiftFormatTests // // Created by Nick Lockwood on 02/10/2021. // Copyright © 2021 Nick Lockwood. All rights reserved. // https://some.example.com // /// func func foo() {} """ let output = """ /// func func foo() {} """ let options = FormatOptions(fileHeader: "") testFormatting(for: input, output, rule: .fileHeader, options: options) } func testReplaceHeaderWhenFileContainsNoCode() { let input = """ // foobar """ let options = FormatOptions(fileHeader: "// foobar") testFormatting(for: input, rule: .fileHeader, options: options, exclude: [.linebreakAtEndOfFile]) } func testReplaceHeaderWhenFileContainsNoCode2() { let input = """ // foobar """ let options = FormatOptions(fileHeader: "// foobar") testFormatting(for: input, rule: .fileHeader, options: options) } func testMultilineCommentHeader() { let input = """ /****************************/ /* Created by Nick Lockwood */ /****************************/ /// func func foo() {} """ let output = """ /// func func foo() {} """ let options = FormatOptions(fileHeader: "") testFormatting(for: input, output, rule: .fileHeader, options: options) } func testNoStripHeaderWhenDisabled() { let input = """ // // test.swift // SwiftFormat // // Created by Nick Lockwood on 08/11/2016. // Copyright © 2016 Nick Lockwood. All rights reserved. // /// func func foo() {} """ let options = FormatOptions(fileHeader: .ignore) testFormatting(for: input, rule: .fileHeader, options: options) } func testNoStripComment() { let input = """ /// func func foo() {} """ let options = FormatOptions(fileHeader: "") testFormatting(for: input, rule: .fileHeader, options: options) } func testNoStripPackageHeader() { let input = """ // swift-tools-version:4.2 import PackageDescription """ let options = FormatOptions(fileHeader: "") testFormatting(for: input, rule: .fileHeader, options: options) } func testNoStripFormatDirective() { let input = """ // swiftformat:options --swiftversion 5.2 import PackageDescription """ let options = FormatOptions(fileHeader: "") testFormatting(for: input, rule: .fileHeader, options: options) } func testNoStripFormatDirectiveAfterHeader() { let input = """ // header // swiftformat:options --swiftversion 5.2 import PackageDescription """ let options = FormatOptions(fileHeader: "") testFormatting(for: input, rule: .fileHeader, options: options) } func testNoReplaceFormatDirective() { let input = """ // swiftformat:options --swiftversion 5.2 import PackageDescription """ let output = """ // Hello World // swiftformat:options --swiftversion 5.2 import PackageDescription """ let options = FormatOptions(fileHeader: "// Hello World") testFormatting(for: input, output, rule: .fileHeader, options: options) } func testSetSingleLineHeader() { let input = """ // // test.swift // SwiftFormat // // Created by Nick Lockwood on 08/11/2016. // Copyright © 2016 Nick Lockwood. All rights reserved. // /// func func foo() {} """ let output = """ // Hello World /// func func foo() {} """ let options = FormatOptions(fileHeader: "// Hello World") testFormatting(for: input, output, rule: .fileHeader, options: options) } func testSetMultilineHeader() { let input = """ // // test.swift // SwiftFormat // // Created by Nick Lockwood on 08/11/2016. // Copyright © 2016 Nick Lockwood. All rights reserved. // /// func func foo() {} """ let output = """ // Hello // World /// func func foo() {} """ let options = FormatOptions(fileHeader: "// Hello\n// World") testFormatting(for: input, output, rule: .fileHeader, options: options) } func testSetMultilineHeaderWithMarkup() { let input = """ // // test.swift // SwiftFormat // // Created by Nick Lockwood on 08/11/2016. // Copyright © 2016 Nick Lockwood. All rights reserved. // /// func func foo() {} """ let output = """ /*--- Hello ---*/ /*--- World ---*/ /// func func foo() {} """ let options = FormatOptions(fileHeader: "/*--- Hello ---*/\n/*--- World ---*/") testFormatting(for: input, output, rule: .fileHeader, options: options) } func testNoStripHeaderIfRuleDisabled() { let input = """ // swiftformat:disable fileHeader // test // swiftformat:enable fileHeader func foo() {} """ let options = FormatOptions(fileHeader: "") testFormatting(for: input, rule: .fileHeader, options: options) } func testNoStripHeaderIfNextRuleDisabled() { let input = """ // swiftformat:disable:next fileHeader // test func foo() {} """ let options = FormatOptions(fileHeader: "") testFormatting(for: input, rule: .fileHeader, options: options) } func testNoStripHeaderDocWithNewlineBeforeCode() { let input = """ /// Header doc class Foo {} """ let options = FormatOptions(fileHeader: "") testFormatting(for: input, rule: .fileHeader, options: options, exclude: [.docComments]) } func testNoDuplicateHeaderIfMissingTrailingBlankLine() { let input = """ // Header comment class Foo {} """ let output = """ // Header comment class Foo {} """ let options = FormatOptions(fileHeader: "Header comment") testFormatting(for: input, output, rule: .fileHeader, options: options) } func testNoDuplicateHeaderContainingPossibleCommentDirective() { let input = """ // Copyright (c) 2010-2023 Foobar // // SPDX-License-Identifier: EPL-2.0 class Foo {} """ let output = """ // Copyright (c) 2010-2024 Foobar // // SPDX-License-Identifier: EPL-2.0 class Foo {} """ let options = FormatOptions(fileHeader: "// Copyright (c) 2010-2024 Foobar\n//\n// SPDX-License-Identifier: EPL-2.0") testFormatting(for: input, output, rule: .fileHeader, options: options) } func testNoDuplicateHeaderContainingCommentDirective() { let input = """ // Copyright (c) 2010-2023 Foobar // // swiftformat:disable all class Foo {} """ let output = """ // Copyright (c) 2010-2024 Foobar // // swiftformat:disable all class Foo {} """ let options = FormatOptions(fileHeader: "// Copyright (c) 2010-2024 Foobar\n//\n// swiftformat:disable all") testFormatting(for: input, output, rule: .fileHeader, options: options) } func testFileHeaderYearReplacement() { let input = """ let foo = bar """ let output: String = { let formatter = DateFormatter() formatter.dateFormat = "yyyy" return "// Copyright © \(formatter.string(from: Date()))\n\nlet foo = bar" }() let options = FormatOptions(fileHeader: "// Copyright © {year}") testFormatting(for: input, output, rule: .fileHeader, options: options) } func testFileHeaderCreationYearReplacement() { let input = """ let foo = bar """ let date = Date(timeIntervalSince1970: 0) let output: String = { let formatter = DateFormatter() formatter.dateFormat = "yyyy" return "// Copyright © \(formatter.string(from: date))\n\nlet foo = bar" }() let fileInfo = FileInfo(creationDate: date) let options = FormatOptions(fileHeader: "// Copyright © {created.year}", fileInfo: fileInfo) testFormatting(for: input, output, rule: .fileHeader, options: options) } func testFileHeaderAuthorReplacement() { let name = """ Test User """ let email = """ test@email.com """ let input = """ let foo = bar """ let output = """ // Created by \(name) \(email) let foo = bar """ let fileInfo = FileInfo(replacements: [.authorName: .constant(name), .authorEmail: .constant(email)]) let options = FormatOptions(fileHeader: "// Created by {author.name} {author.email}", fileInfo: fileInfo) testFormatting(for: input, output, rule: .fileHeader, options: options) } func testFileHeaderAuthorReplacement2() { let author = """ Test User """ let input = """ let foo = bar """ let output = """ // Created by \(author) let foo = bar """ let fileInfo = FileInfo(replacements: [.author: .constant(author)]) let options = FormatOptions(fileHeader: "// Created by {author}", fileInfo: fileInfo) testFormatting(for: input, output, rule: .fileHeader, options: options) } func testFileHeaderMultipleReplacement() { let name = """ Test User """ let input = """ let foo = bar """ let output = """ // Copyright © \(name) // Created by \(name) let foo = bar """ let fileInfo = FileInfo(replacements: [.authorName: .constant(name)]) let options = FormatOptions(fileHeader: "// Copyright © {author.name}\n// Created by {author.name}", fileInfo: fileInfo) testFormatting(for: input, output, rule: .fileHeader, options: options) } func testFileHeaderCreationDateReplacement() { let input = """ let foo = bar """ let date = Date(timeIntervalSince1970: 0) let output: String = { let formatter = DateFormatter() formatter.dateStyle = .short formatter.timeStyle = .none return "// Created by Nick Lockwood on \(formatter.string(from: date)).\n\nlet foo = bar" }() let fileInfo = FileInfo(creationDate: date) let options = FormatOptions(fileHeader: "// Created by Nick Lockwood on {created}.", fileInfo: fileInfo) testFormatting(for: input, output, rule: .fileHeader, options: options) } func testFileHeaderDateFormattingIso() { let date = createTestDate("2023-08-09") let input = """ let foo = bar """ let output = """ // 2023-08-09 let foo = bar """ let fileInfo = FileInfo(creationDate: date) let options = FormatOptions(fileHeader: "// {created}", dateFormat: .iso, fileInfo: fileInfo) testFormatting(for: input, output, rule: .fileHeader, options: options) } func testFileHeaderDateFormattingDayMonthYear() { let date = createTestDate("2023-08-09") let input = """ let foo = bar """ let output = """ // 09/08/2023 let foo = bar """ let fileInfo = FileInfo(creationDate: date) let options = FormatOptions(fileHeader: "// {created}", dateFormat: .dayMonthYear, fileInfo: fileInfo) testFormatting(for: input, output, rule: .fileHeader, options: options) } func testFileHeaderDateFormattingMonthDayYear() { let date = createTestDate("2023-08-09") let input = """ let foo = bar """ let output = """ // 08/09/2023 let foo = bar """ let fileInfo = FileInfo(creationDate: date) let options = FormatOptions(fileHeader: "// {created}", dateFormat: .monthDayYear, fileInfo: fileInfo) testFormatting(for: input, output, rule: .fileHeader, options: options) } func testFileHeaderDateFormattingCustom() { let date = createTestDate("2023-08-09T12:59:30.345Z", .timestamp) let input = """ let foo = bar """ let output = """ // 23.08.09-12.59.30.345 let foo = bar """ let fileInfo = FileInfo(creationDate: date) let options = FormatOptions(fileHeader: "// {created}", dateFormat: .custom("yy.MM.dd-HH.mm.ss.SSS"), timeZone: .identifier("UTC"), fileInfo: fileInfo) testFormatting(for: input, output, rule: .fileHeader, options: options) } func testFileHeaderFileReplacement() { let input = """ let foo = bar """ let output = """ // MyFile.swift let foo = bar """ let fileInfo = FileInfo(filePath: "~/MyFile.swift") let options = FormatOptions(fileHeader: "// {file}", fileInfo: fileInfo) testFormatting(for: input, output, rule: .fileHeader, options: options) } func testEdgeCaseHeaderEndIndexPlusNewHeaderTokensCountEqualsFileTokensEndIndex() { let input = """ // Header comment class Foo {} """ let output = """ // Header line1 // Header line2 class Foo {} """ let options = FormatOptions(fileHeader: "// Header line1\n// Header line2") testFormatting(for: input, output, rule: .fileHeader, options: options) } func testFileHeaderBlankLineNotRemovedBeforeFollowingComment() { let input = """ // // Header // // Something else... """ let options = FormatOptions(fileHeader: "//\n// Header\n//") testFormatting(for: input, rule: .fileHeader, options: options) } func testFileHeaderBlankLineNotRemovedBeforeFollowingComment2() { let input = """ // // Header // // // Something else... // """ let options = FormatOptions(fileHeader: "//\n// Header\n//") testFormatting(for: input, rule: .fileHeader, options: options) } func testFileHeaderRemovedAfterHashbang() { let input = """ #!/usr/bin/swift // Header line1 // Header line2 let foo = 5 """ let output = """ #!/usr/bin/swift let foo = 5 """ let options = FormatOptions(fileHeader: "") testFormatting(for: input, output, rule: .fileHeader, options: options) } func testFileHeaderPlacedAfterHashbang() { let input = """ #!/usr/bin/swift let foo = 5 """ let output = """ #!/usr/bin/swift // Header line1 // Header line2 let foo = 5 """ let options = FormatOptions(fileHeader: "// Header line1\n// Header line2") testFormatting(for: input, output, rule: .fileHeader, options: options) } func testBlankLineAfterHashbangNotRemovedByFileHeader() { let input = """ #!/usr/bin/swift let foo = 5 """ let options = FormatOptions(fileHeader: "") testFormatting(for: input, rule: .fileHeader, options: options) } func testLineAfterHashbangNotAffectedByFileHeaderRemoval() { let input = """ #!/usr/bin/swift let foo = 5 """ let options = FormatOptions(fileHeader: "") testFormatting(for: input, rule: .fileHeader, options: options) } func testDisableFileHeaderCommentRespectedAfterHashbang() { let input = """ #!/usr/bin/swift // swiftformat:disable fileHeader // Header line1 // Header line2 let foo = 5 """ let options = FormatOptions(fileHeader: "") testFormatting(for: input, rule: .fileHeader, options: options) } func testDisableFileHeaderCommentRespectedAfterHashbang2() { let input = """ #!/usr/bin/swift // swiftformat:disable fileHeader // Header line1 // Header line2 let foo = 5 """ let options = FormatOptions(fileHeader: "") testFormatting(for: input, rule: .fileHeader, options: options) } private func testTimeZone( timeZone: FormatTimeZone, tests: [String: String] ) { for (input, expected) in tests { let date = createTestDate(input, .time) let input = """ let foo = bar """ let output = """ // \(expected) let foo = bar """ let fileInfo = FileInfo(creationDate: date) let options = FormatOptions( fileHeader: "// {created}", dateFormat: .custom("HH:mm"), timeZone: timeZone, fileInfo: fileInfo ) testFormatting(for: input, output, rule: .fileHeader, options: options) } } func testFileHeaderDateTimeZoneSystem() { let baseDate = createTestDate("15:00Z", .time) let offset = TimeZone.current.secondsFromGMT(for: baseDate) let date = baseDate.addingTimeInterval(Double(offset)) let formatter = DateFormatter() formatter.dateFormat = "HH:mm" formatter.timeZone = TimeZone(secondsFromGMT: 0) let expected = formatter.string(from: date) testTimeZone(timeZone: .system, tests: [ "15:00Z": expected, "16:00+1": expected, "01:00+10": expected, "16:30+0130": expected, ]) } func testFileHeaderDateTimeZoneAbbreviations() throws { // GMT+0530 try testTimeZone(timeZone: XCTUnwrap(FormatTimeZone(rawValue: "IST")), tests: [ "15:00Z": "20:30", "16:00+1": "20:30", "01:00+10": "20:30", "16:30+0130": "20:30", ]) } func testFileHeaderDateTimeZoneIdentifiers() throws { // GMT+0845 try testTimeZone(timeZone: XCTUnwrap(FormatTimeZone(rawValue: "Australia/Eucla")), tests: [ "15:00Z": "23:45", "16:00+1": "23:45", "01:00+10": "23:45", "16:30+0130": "23:45", ]) } func testGitHelpersReturnsInfo() { let info = GitFileInfo(url: URL(fileURLWithPath: #file)) XCTAssertNotNil(info?.authorName) XCTAssertNotNil(info?.authorEmail) XCTAssertNotNil(info?.creationDate) } func testGitHelpersWorksWithFilesNotCommitedYet() throws { try withTempProjectFile { url in let info = GitFileInfo(url: url) XCTAssertNotNil(info?.authorName) XCTAssertNotNil(info?.authorEmail) XCTAssertNil(info?.creationDate) } } func testFileHeaderRuleThrowsIfCreationDateUnavailable() { let input = """ let foo = bar """ let options = FormatOptions(fileHeader: "// Created by Nick Lockwood on {created}.", fileInfo: FileInfo()) XCTAssertThrowsError(try format(input, rules: [.fileHeader], options: options)) } func testFileHeaderRuleThrowsIfFileNameUnavailable() { let input = """ let foo = bar """ let options = FormatOptions(fileHeader: "// {file}.", fileInfo: FileInfo()) XCTAssertThrowsError(try format(input, rules: [.fileHeader], options: options)) } func testFileHeaderWithFilePathButNoCreationDate() { let input = """ let foo = bar """ let output = """ // File: test.swift let foo = bar """ let fileInfo = FileInfo(filePath: "/path/to/test.swift", creationDate: nil) let options = FormatOptions(fileHeader: "// File: {file}", fileInfo: fileInfo) testFormatting(for: input, output, rule: .fileHeader, options: options) } func testFileHeaderWithFilePathButNoCreationDateDoesNotUseCreatedPlaceholder() { let input = """ let foo = bar """ let fileInfo = FileInfo(filePath: "/path/to/test.swift", creationDate: nil) let options = FormatOptions(fileHeader: "// Created: {created}", fileInfo: fileInfo) XCTAssertThrowsError(try format(input, rules: [.fileHeader], options: options)) } func testFileHeaderWithExistingHeaderAndNoCreationDate() { let input = """ // Existing header // Created on 2020-01-01 let foo = bar """ let fileInfo = FileInfo(filePath: "/path/to/test.swift", creationDate: nil) let options = FormatOptions(fileHeader: "// New header\n// Created: {created}", fileInfo: fileInfo) // When creation date is unavailable and template uses {created}, throws error even if file has existing header XCTAssertThrowsError(try format(input, rules: [.fileHeader], options: options)) } } private enum TestDateFormat: String { case basic = "yyyy-MM-dd" case time = "HH:mmZZZZZ" case timestamp = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" } private func createTestDate( _ input: String, _ format: TestDateFormat = .basic ) -> Date { let formatter = DateFormatter() formatter.dateFormat = format.rawValue formatter.timeZone = .current return formatter.date(from: input)! } private func withTempProjectFile(fn: (URL) -> Void) throws { let prefix = UUID().uuidString var components = URL(fileURLWithPath: #file) .pathComponents .prefix(while: { $0 != "Tests" }) components.append(".temp") let directory = components.joined(separator: "/") if !FileManager.default.fileExists(atPath: directory) { try FileManager.default.createDirectory(atPath: directory, withIntermediateDirectories: true) } let url = URL(fileURLWithPath: directory).appendingPathComponent(prefix + ".swift") FileManager.default.createFile(atPath: url.path, contents: nil) fn(url) try FileManager.default.removeItem(at: url) }