diff --git a/CHANGELOG.md b/CHANGELOG.md index d210ab4642..12a5d2d711 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,10 @@ * Include the configured `bind_identifier` in `self_binding` violation messages. [JP Simard](https://github.com/jpims) + +* Add `library_content_provider` file type to `file_types_order` rule + to allow `LibraryContentProvider` to be ordered independent from `main_type`. + [dahlborn](https://github.com/dahlborn) #### Bug Fixes diff --git a/Source/SwiftLintFramework/Rules/RuleConfigurations/FileTypesOrderConfiguration.swift b/Source/SwiftLintFramework/Rules/RuleConfigurations/FileTypesOrderConfiguration.swift index 0d1a78b2a7..0acf7aad6b 100644 --- a/Source/SwiftLintFramework/Rules/RuleConfigurations/FileTypesOrderConfiguration.swift +++ b/Source/SwiftLintFramework/Rules/RuleConfigurations/FileTypesOrderConfiguration.swift @@ -3,6 +3,7 @@ enum FileType: String { case mainType = "main_type" case `extension` = "extension" case previewProvider = "preview_provider" + case libraryContentProvider = "library_content_provider" } public struct FileTypesOrderConfiguration: RuleConfiguration, Equatable { @@ -11,7 +12,8 @@ public struct FileTypesOrderConfiguration: RuleConfiguration, Equatable { [.supportingType], [.mainType], [.extension], - [.previewProvider] + [.previewProvider], + [.libraryContentProvider] ] public var consoleDescription: String { diff --git a/Source/SwiftLintFramework/Rules/Style/FileTypesOrderRule.swift b/Source/SwiftLintFramework/Rules/Style/FileTypesOrderRule.swift index 7917f7e882..6b32308087 100644 --- a/Source/SwiftLintFramework/Rules/Style/FileTypesOrderRule.swift +++ b/Source/SwiftLintFramework/Rules/Style/FileTypesOrderRule.swift @@ -20,7 +20,7 @@ public struct FileTypesOrderRule: ConfigurationProviderRule, OptInRule { // swiftlint:disable:next function_body_length public func validate(file: SwiftLintFile) -> [StyleViolation] { guard let mainTypeSubstructure = mainTypeSubstructure(in: file), - let mainTypeSubstuctureOffset = mainTypeSubstructure.offset else { return [] } + let mainTypeSubstuctureOffset = mainTypeSubstructure.offset else { return [] } let extensionsSubstructures = self.extensionsSubstructures( in: file, @@ -32,25 +32,28 @@ public struct FileTypesOrderRule: ConfigurationProviderRule, OptInRule { mainTypeSubstructure: mainTypeSubstructure ) - let previewProviderSubstructures = self.previewProviderSubstructures(in: file) + let previewProviderSubstructures = self.substructures( + in: file, + withInheritedType: "PreviewProvider" + ) - let mainTypeOffset: [FileTypeOffset] = [(.mainType, mainTypeSubstuctureOffset)] - let extensionOffsets: [FileTypeOffset] = extensionsSubstructures.compactMap { substructure in - guard let offset = substructure.offset else { return nil } - return (.extension, offset) - } + let libraryContentSubstructures = self.substructures( + in: file, + withInheritedType: "LibraryContentProvider" + ) - let supportingTypeOffsets: [FileTypeOffset] = supportingTypesSubstructures.compactMap { substructure in - guard let offset = substructure.offset else { return nil } - return (.supportingType, offset) - } + let mainTypeOffset: [FileTypeOffset] = [(.mainType, mainTypeSubstuctureOffset)] + let extensionOffsets: [FileTypeOffset] = extensionsSubstructures.offsets(for: .extension) + let supportingTypeOffsets: [FileTypeOffset] = supportingTypesSubstructures.offsets(for: .supportingType) + let previewProviderOffsets: [FileTypeOffset] = previewProviderSubstructures.offsets(for: .previewProvider) + let libraryContentOffsets: [FileTypeOffset] = libraryContentSubstructures.offsets(for: .libraryContentProvider) - let previewProviderOffsets: [FileTypeOffset] = previewProviderSubstructures.compactMap { substructure in - guard let offset = substructure.offset else { return nil } - return (.previewProvider, offset) - } + let allOffsets = mainTypeOffset + + extensionOffsets + + supportingTypeOffsets + + previewProviderOffsets + + libraryContentOffsets - let allOffsets = mainTypeOffset + extensionOffsets + supportingTypeOffsets + previewProviderOffsets let orderedFileTypeOffsets = allOffsets.sorted { lhs, rhs in lhs.offset < rhs.offset } var violations = [StyleViolation]() @@ -97,8 +100,8 @@ public struct FileTypesOrderRule: ConfigurationProviderRule, OptInRule { let dict = file.structureDictionary return dict.substructure.filter { substructure in guard let kind = substructure.kind else { return false } - return substructure.offset != mainTypeSubstructure.offset && - kind.contains(SwiftDeclarationKind.extension.rawValue) + return substructure.offset != mainTypeSubstructure.offset + && kind.contains(SwiftDeclarationKind.extension.rawValue) } } @@ -112,16 +115,19 @@ public struct FileTypesOrderRule: ConfigurationProviderRule, OptInRule { let dict = file.structureDictionary return dict.substructure.filter { substructure in guard let declarationKind = substructure.declarationKind else { return false } - guard !substructure.inheritedTypes.contains("PreviewProvider") else { return false } + guard !substructure.hasExcludedInheritedType else { return false } - return substructure.offset != mainTypeSubstructure.offset && - supportingTypeKinds.contains(declarationKind) + return substructure.offset != mainTypeSubstructure.offset + && supportingTypeKinds.contains(declarationKind) } } - private func previewProviderSubstructures(in file: SwiftLintFile) -> [SourceKittenDictionary] { - return file.structureDictionary.substructure.filter { substructure in - return substructure.inheritedTypes.contains("PreviewProvider") + private func substructures( + in file: SwiftLintFile, + withInheritedType inheritedType: String + ) -> [SourceKittenDictionary] { + file.structureDictionary.substructure.filter { substructure in + substructure.inheritedTypes.contains(inheritedType) } } @@ -147,16 +153,33 @@ public struct FileTypesOrderRule: ConfigurationProviderRule, OptInRule { let priorityKindSubstructures = dict.substructure.filter { substructure in guard let kind = substructure.declarationKind else { return false } - guard !substructure.inheritedTypes.contains("PreviewProvider") else { return false } + guard !substructure.hasExcludedInheritedType else { return false } return priorityKinds.contains(kind) } let substructuresSortedByBodyLength = priorityKindSubstructures.sorted { lhs, rhs in - return (lhs.bodyLength ?? 0) > (rhs.bodyLength ?? 0) + (lhs.bodyLength ?? 0) > (rhs.bodyLength ?? 0) } // specify class, enum or struct with longest body as main type return substructuresSortedByBodyLength.first } } + +private extension SourceKittenDictionary { + var hasExcludedInheritedType: Bool { + self.inheritedTypes.contains { inheritedType in + inheritedType == "PreviewProvider" || inheritedType == "LibraryContentProvider" + } + } +} + +private extension Array where Element == SourceKittenDictionary { + func offsets(for fileType: FileType) -> [(fileType: FileType, offset: ByteCount)] { + self.compactMap { substructure in + guard let offset = substructure.offset else { return nil } + return (fileType, offset) + } + } +} diff --git a/Source/SwiftLintFramework/Rules/Style/FileTypesOrderRuleExamples.swift b/Source/SwiftLintFramework/Rules/Style/FileTypesOrderRuleExamples.swift index 173a3e16b3..dda6b8ef0e 100644 --- a/Source/SwiftLintFramework/Rules/Style/FileTypesOrderRuleExamples.swift +++ b/Source/SwiftLintFramework/Rules/Style/FileTypesOrderRuleExamples.swift @@ -1,3 +1,4 @@ +// swiftlint:disable:next type_body_length internal struct FileTypesOrderRuleExamples { static let defaultOrderParts = [ """ @@ -120,15 +121,24 @@ internal struct FileTypesOrderRuleExamples { } """), Example(""" + // Main Type struct ContentView: View { var body: some View { Text("Hello, World!") } } + // Preview Provider struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } + + // Library Content Provider + struct ContentView_LibraryContent: LibraryContentProvider { + var views: [LibraryItem] { + LibraryItem(ContentView()) + } + } """) ] @@ -188,10 +198,31 @@ internal struct FileTypesOrderRuleExamples { """), Example(""" // Preview Provider - ↓struct ContentView_Previews: PreviewProvider {} + ↓struct ContentView_Previews: PreviewProvider { + static var previews: some View { ContentView() } + } // Main Type - struct ContentView: View {} + struct ContentView: View { + var body: some View { + Text("Hello, World!") + } + } + """), + Example(""" + // Library Content Provider + ↓struct ContentView_LibraryContent: LibraryContentProvider { + var views: [LibraryItem] { + LibraryItem(ContentView()) + } + } + + // Main Type + struct ContentView: View { + var body: some View { + Text("Hello, World!") + } + } """) ] } diff --git a/Tests/SwiftLintFrameworkTests/FileTypesOrderRuleTests.swift b/Tests/SwiftLintFrameworkTests/FileTypesOrderRuleTests.swift index 2016aa0b42..f775a14e27 100644 --- a/Tests/SwiftLintFrameworkTests/FileTypesOrderRuleTests.swift +++ b/Tests/SwiftLintFrameworkTests/FileTypesOrderRuleTests.swift @@ -55,6 +55,19 @@ class FileTypesOrderRuleTests: XCTestCase { struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } + """), + Example(""" + ↓struct ContentView: View { + var body: some View { + Text("Hello, World!") + } + } + + struct ContentView_LibraryContent: LibraryContentProvider { + var views: [LibraryItem] { + LibraryItem(ContentView()) + } + } """) ] @@ -65,7 +78,7 @@ class FileTypesOrderRuleTests: XCTestCase { verifyRule( reversedOrderDescription, ruleConfiguration: [ - "order": ["preview_provider", "extension", "main_type", "supporting_type"] + "order": ["library_content_provider", "preview_provider", "extension", "main_type", "supporting_type"] ] ) }