From 5f66f177da02763633bc51c4c4ffbc03ba610ab3 Mon Sep 17 00:00:00 2001 From: Marquis Date: Sat, 18 Jan 2025 15:11:14 -0500 Subject: [PATCH] Add SwiftLint rules to project SwiftLint will ensure consistent rules such as public types using the appropriate prefix, valid identifier names, etc. Co-authored-by: Grant Neufeld --- .swiftlint.yml | 45 +++++++ Package.resolved | 11 +- Package.swift | 4 +- .../PuzzleKit/Generic/PKFloodFillable.swift | 4 +- .../PuzzleKit/Generic/PKGridCoordinate.swift | 5 +- Sources/PuzzleKit/Generic/PKGridRegion.swift | 6 +- .../PuzzleKit/Generic/StringExtensions.swift | 2 +- Sources/PuzzleKit/Taiji/PKTaijiDecoder.swift | 21 +-- Sources/PuzzleKit/Taiji/PKTaijiEncoder.swift | 24 ++-- Sources/PuzzleKit/Taiji/PKTaijiPuzzle.swift | 14 +- .../Taiji/PKTaijiPuzzleValidator.swift | 122 ++++++++++-------- 11 files changed, 165 insertions(+), 93 deletions(-) create mode 100644 .swiftlint.yml diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..bb85507 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,45 @@ +included: + - Sources + +allow_zero_lintable_files: false +strict: false +lenient: false +check_for_updates: true +force_cast: warning +force_try: + severity: warning +line_length: 120 +type_body_length: + - 300 + - 400 +file_length: + warning: 500 + error: 1200 + +type_name: + min_length: 3 + max_length: + warning: 40 + error: 50 + excluded: iPhone + allowed_symbols: ["_"] +identifier_name: + min_length: + error: 2 + excluded: + - x + - y + - up + - id + - URL + - GlobalAPIKey +custom_rules: + pk_header: + name: "Consistent PuzzleKit Header" + regex: "(public|open) (final +)?(class|enum|struct|protocol) ([^P].|.[^K])" + capture_group: 4 + match_kinds: + - identifier + message: "Public-facing types must be prefixed with 'PK'." + severity: warning +reporter: "xcode" \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index 2969d91..1482826 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "6230822fda4d48a6effa9ee9a66ef003d919801cc84bb804a93e1bcc9b6817c8", + "originHash" : "46446cc224ca4a72f5f8b34e6bd41077e94aa39f86e3cda5ed4c903db2f7f13f", "pins" : [ { "identity" : "swift-docc-plugin", @@ -18,6 +18,15 @@ "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", "version" : "1.0.0" } + }, + { + "identity" : "swiftlintplugins", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SimplyDanny/SwiftLintPlugins", + "state" : { + "revision" : "7a3d77f3dd9f91d5cea138e52c20cfceabf352de", + "version" : "0.58.2" + } } ], "version" : 3 diff --git a/Package.swift b/Package.swift index fbcf358..c115b2e 100644 --- a/Package.swift +++ b/Package.swift @@ -14,12 +14,14 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"), + .package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", from: "0.58.2"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( - name: "PuzzleKit"), + name: "PuzzleKit", + plugins: [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins")]), .testTarget( name: "PuzzleKitTests", dependencies: ["PuzzleKit"] diff --git a/Sources/PuzzleKit/Generic/PKFloodFillable.swift b/Sources/PuzzleKit/Generic/PKFloodFillable.swift index c2bd4dd..603833a 100644 --- a/Sources/PuzzleKit/Generic/PKFloodFillable.swift +++ b/Sources/PuzzleKit/Generic/PKFloodFillable.swift @@ -32,7 +32,7 @@ extension PKGrid where Self: PKFloodFillable { var stack = [origin] var visited = Set() var region = Set() - var criterion: Criteria? = nil + var criterion: Criteria? while !stack.isEmpty { guard let top = stack.popLast(), let tile = self.tile(at: top) else { @@ -69,7 +69,7 @@ extension PKGrid where Self: PKFloodFillable { stack.append(down) } } - + return region } } diff --git a/Sources/PuzzleKit/Generic/PKGridCoordinate.swift b/Sources/PuzzleKit/Generic/PKGridCoordinate.swift index e54043a..a29b9d0 100644 --- a/Sources/PuzzleKit/Generic/PKGridCoordinate.swift +++ b/Sources/PuzzleKit/Generic/PKGridCoordinate.swift @@ -56,7 +56,7 @@ public struct PKGridCoordinate: Hashable, Equatable, Sendable { } // - MARK: Neighbor Coordinates (Non-nullable/Coalescing) - + /// Retrieves the coordinate above. public func above() -> Self { return .init(x: self.x, y: max(1, self.y - 1)) @@ -99,7 +99,6 @@ public struct PKGridCoordinate: Hashable, Equatable, Sendable { return .init(x: max(1, self.x - 1), y: self.y) } - /// Returns the coordinate below, or nil if the expected coordinate reaches out of bounds. /// - Parameter maximum: The maximum coordinate on the Y axis. public func below(stoppingAt maximum: Int? = nil) -> Self? { @@ -140,7 +139,7 @@ public extension PKGridCoordinate { } /// Subtracts a scalar integer value from a grid coordinate. - static func - (lhs:PKGridCoordinate, rhs: Int) -> PKGridCoordinate { + static func - (lhs: PKGridCoordinate, rhs: Int) -> PKGridCoordinate { .init(x: rhs - lhs.x, y: rhs - lhs.y) } } diff --git a/Sources/PuzzleKit/Generic/PKGridRegion.swift b/Sources/PuzzleKit/Generic/PKGridRegion.swift index 169b9a3..9292733 100644 --- a/Sources/PuzzleKit/Generic/PKGridRegion.swift +++ b/Sources/PuzzleKit/Generic/PKGridRegion.swift @@ -42,7 +42,8 @@ public struct PKGridRegion: Hashable, Equatable, Identifiable { } /// Returns an array of coordinates that describe the shape of the region, relative to an origin coordinate. - /// - Parameter origin: The origin tile to reference. If none is provided, the first tile in the region will be used. + /// - Parameter origin: The origin tile to reference. If none is provided, the first tile in the region will be + /// used. public func shape(relativeTo origin: PKGridCoordinate? = nil) -> [PKGridCoordinate] { guard let realOrigin = origin ?? coordinates.first else { return [] } return coordinates.map { $0 - realOrigin } @@ -54,7 +55,8 @@ public extension PKGridRegion { /// - Parameter origin: The origin tile to start flood-filling from. /// - Parameter grid: The grid to flood-fill into. /// - Parameter id: The region's unique identifier. - init(floodFillingFrom origin: PKGridCoordinate, in grid: Grid, identifiedBy id: Int) where Grid: PKGrid & PKFloodFillable { + init(floodFillingFrom origin: PKGridCoordinate, in grid: Grid, identifiedBy id: Int) + where Grid: PKGrid & PKFloodFillable { let region = grid.findFloodFilledRegion(startingAt: origin) coordinates = Array(region) self.id = id diff --git a/Sources/PuzzleKit/Generic/StringExtensions.swift b/Sources/PuzzleKit/Generic/StringExtensions.swift index b39e1c1..bcdbb56 100644 --- a/Sources/PuzzleKit/Generic/StringExtensions.swift +++ b/Sources/PuzzleKit/Generic/StringExtensions.swift @@ -15,7 +15,7 @@ extension String { let idx = self.index(self.startIndex, offsetBy: offset) return self[idx] } - + init?(charCode: UInt32) { guard let scalar = UnicodeScalar(charCode) else { return nil } let char = Character(scalar) diff --git a/Sources/PuzzleKit/Taiji/PKTaijiDecoder.swift b/Sources/PuzzleKit/Taiji/PKTaijiDecoder.swift index a8e7fc5..397a882 100644 --- a/Sources/PuzzleKit/Taiji/PKTaijiDecoder.swift +++ b/Sources/PuzzleKit/Taiji/PKTaijiDecoder.swift @@ -6,6 +6,7 @@ // enum PKTaijiDecoder { + typealias DecodeError = PKTaijiPuzzleDecoderError struct Constants: Sendable { static let digits: String = "1234567890" static let upperAlphabet: String = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -38,7 +39,8 @@ enum PKTaijiDecoder { case error(PKTaijiPuzzleDecoderError) } - static func decode(from source: String) throws(PKTaijiPuzzleDecoderError) -> (Int, [PKTaijiTile], PKTaijiMechanics) { + // swiftlint:disable:next function_body_length cyclomatic_complexity large_tuple + static func decode(from source: String) throws(DecodeError) -> (Int, [PKTaijiTile], PKTaijiMechanics) { var tiles = [PKTaijiTile]() var mechanics: PKTaijiMechanics = [] var boardWidth = 0 @@ -52,7 +54,7 @@ enum PKTaijiDecoder { case let (char, .initial) where Constants.digits.contains(char), let (char, .getWidth) where Constants.digits.contains(char): if state == .initial { state = .getWidth } - widthString = widthString + String(char) + widthString += String(char) case (":", .getWidth): guard let convertedNumber = Int(widthString) else { throw .invalidBoardWidth @@ -62,7 +64,8 @@ enum PKTaijiDecoder { case (Constants.fillEmpty, .scanForTile): state = .prefillArray(invisible: false) - // NOTE: Check for the changes here, because doing so after skipping the attributes is too much to handle. + // NOTE: Check for the changes here, because doing so after skipping the attributes is too much to + // handle. if let lastTile = tiles.last, lastTile.state != .normal, lastTile.filled { filledSymbolicTile = false } @@ -93,7 +96,7 @@ enum PKTaijiDecoder { value = abs(9 - value) } var tile = PKTaijiTile.symbolic(.dot(value: value, additive: additive)) - if let (extendedAttrs, readChars) = Self.getExtendedAttributes(after: charIndex, in: source) { + if let (extendedAttrs, readChars) = Self.getAttributes(after: charIndex, in: source) { tile = tile.applying(attributes: extendedAttrs) state = .readExtendedAttributes extendedAttrsChars = readChars @@ -109,7 +112,7 @@ enum PKTaijiDecoder { } let value = Constants.flowers.distance(to: index) var tile = PKTaijiTile.symbolic(.flower(petals: value)) - if let (extendedAttrs, readChars) = Self.getExtendedAttributes(after: charIndex, in: source) { + if let (extendedAttrs, readChars) = Self.getAttributes(after: charIndex, in: source) { tile = tile.applying(attributes: extendedAttrs) state = .readExtendedAttributes extendedAttrsChars = readChars @@ -121,7 +124,7 @@ enum PKTaijiDecoder { } case (Constants.diamond, .scanForTile): var tile = PKTaijiTile.symbolic(.diamond) - if let (extendedAttrs, readChars) = Self.getExtendedAttributes(after: charIndex, in: source) { + if let (extendedAttrs, readChars) = Self.getAttributes(after: charIndex, in: source) { tile = tile.applying(attributes: extendedAttrs) state = .readExtendedAttributes extendedAttrsChars = readChars @@ -133,7 +136,7 @@ enum PKTaijiDecoder { mechanics.insert(.diamond) case (Constants.dash, .scanForTile), (Constants.slash, .scanForTile): var tile = PKTaijiTile.symbolic(.slashdash(rotates: character == Constants.slash)) - if let (extendedAttrs, readChars) = Self.getExtendedAttributes(after: charIndex, in: source) { + if let (extendedAttrs, readChars) = Self.getAttributes(after: charIndex, in: source) { tile = tile.applying(attributes: extendedAttrs) state = .readExtendedAttributes extendedAttrsChars = readChars @@ -180,7 +183,7 @@ enum PKTaijiDecoder { } } - private static func getExtendedAttributes(after index: Int, in source: String) -> (PKTaijiExtendedAttributes, Int)? { + private static func getAttributes(after index: Int, in source: String) -> (PKTaijiExtendedAttributes, Int)? { var color: PKTaijiSymbolColor? var readChars = 0 var (filled, state) = (false, PKTaijiTileState.normal) @@ -190,7 +193,7 @@ enum PKTaijiDecoder { if Constants.colors.contains(colorCharacter) { color = Constants.colorMap[colorCharacter] ?? .black readChars += 1 - + let attribCharacter = source[source.index(after: strIndex)] if Constants.specialDigits.contains(attribCharacter) { readChars += 1 diff --git a/Sources/PuzzleKit/Taiji/PKTaijiEncoder.swift b/Sources/PuzzleKit/Taiji/PKTaijiEncoder.swift index 18050c5..5950395 100644 --- a/Sources/PuzzleKit/Taiji/PKTaijiEncoder.swift +++ b/Sources/PuzzleKit/Taiji/PKTaijiEncoder.swift @@ -23,7 +23,7 @@ extension PKTaijiTile { } tileCode += String(character) - let color = Constants.colorMap.first { (key: Character, value: PKTaijiSymbolColor) in + let color = Constants.colorMap.first { (_: Character, value: PKTaijiSymbolColor) in value == self.color }?.key if let color { @@ -35,9 +35,13 @@ extension PKTaijiTile { } } } + tileCode += getBackgroundState() + return tileCode + } + private func getBackgroundState() -> String { var stateAttribute = "0" - switch (self.state) { + switch self.state { case .invisible: stateAttribute = "8" case .fixed: @@ -45,26 +49,24 @@ extension PKTaijiTile { case .normal: stateAttribute = self.filled ? "2" : "0" } - tileCode += stateAttribute - - return tileCode + return stateAttribute } } enum PKTaijiEncoder { typealias Constants = PKTaijiDecoder.Constants - + static func encode(_ puzzle: PKTaijiPuzzle) -> String { let prefix = "\(puzzle.width):" var encodedString = puzzle.tiles.map { $0.encode() }.reduce("", +) // NOTE: Reverse the contents of the range, because we want to go top-down instead of bottom-up. - for i in (2...26).reversed() { - guard let character = String(charCode: 64 + i) else { continue } - + for charIdx in (2...26).reversed() { + guard let character = String(charCode: 64 + charIdx) else { continue } + encodedString = encodedString - .replacingOccurrences(of: String(repeating: "0", count: i), with: "+" + character) - .replacingOccurrences(of: String(repeating: "8", count: i), with: "-" + character) + .replacingOccurrences(of: String(repeating: "0", count: charIdx), with: "+" + character) + .replacingOccurrences(of: String(repeating: "8", count: charIdx), with: "-" + character) } return prefix + encodedString diff --git a/Sources/PuzzleKit/Taiji/PKTaijiPuzzle.swift b/Sources/PuzzleKit/Taiji/PKTaijiPuzzle.swift index befba9b..60ac174 100644 --- a/Sources/PuzzleKit/Taiji/PKTaijiPuzzle.swift +++ b/Sources/PuzzleKit/Taiji/PKTaijiPuzzle.swift @@ -175,20 +175,20 @@ extension PKTaijiPuzzle: PKGridStretchable { var copy = self copy.width += 1 - for i in 1...self.height { - let index = i * self.width + for row in 1...self.height { + let index = row * self.width copy.tiles.insert(.empty(), at: index + 1) } - + return copy } - + public func appendingRow() -> PKTaijiPuzzle { var copy = self copy.tiles += Array(repeating: .empty(), count: width) return copy } - + public func removingLastColumn() -> PKTaijiPuzzle { var copy = self copy.width -= 1 @@ -203,10 +203,10 @@ extension PKTaijiPuzzle: PKGridStretchable { copy.tiles.append(tile) column += 1 } - + return copy } - + public func removingLastRow() -> PKTaijiPuzzle { var copy = self copy.tiles.removeLast(copy.width) diff --git a/Sources/PuzzleKit/Taiji/PKTaijiPuzzleValidator.swift b/Sources/PuzzleKit/Taiji/PKTaijiPuzzleValidator.swift index 0eb8bf3..4e1ab87 100644 --- a/Sources/PuzzleKit/Taiji/PKTaijiPuzzleValidator.swift +++ b/Sources/PuzzleKit/Taiji/PKTaijiPuzzleValidator.swift @@ -27,15 +27,15 @@ public class PKTaijiPuzzleValidator { /// Ignore color mechanics when validating. public static let ignoresColor = Options(rawValue: 1 << 0) - + /// Allow color mechanics to match against any type of symbol when necessary. public static let colorsMatchAnySymbols = Options(rawValue: 1 << 1) - + public init(rawValue: Int) { self.rawValue = rawValue } } - + struct SymbolLUT { var diamonds = [PKGridCoordinate]() var dotsPositive = [PKGridCoordinate]() @@ -43,7 +43,7 @@ public class PKTaijiPuzzleValidator { var slashdashes = [PKGridCoordinate]() var flowers = [PKGridCoordinate]() } - + var options: Options var puzzle: PKTaijiPuzzle var symbolLUT = SymbolLUT() @@ -51,15 +51,15 @@ public class PKTaijiPuzzleValidator { var regions = [Int: PKGridRegion]() var totalRegions: Int { return regions.count } - + public init(puzzle: PKTaijiPuzzle, options: Options = []) { self.puzzle = puzzle self.options = options - + var currentRegion = 1 self.regionMap = [PKGridCoordinate: Int]() self.regions = [:] - + for (index, tile) in puzzle.tiles.enumerated() { let tileCoordinate = index.toCoordinate(wrappingAround: puzzle.width) if regionMap[tileCoordinate] == nil { @@ -73,7 +73,7 @@ public class PKTaijiPuzzleValidator { self.updateSymbolLUT(index: index, tile: tile) } } - + public func validate() -> ValidationResult { for flower in symbolLUT.flowers { let result = flowerConstraintsSatisfied(for: flower) @@ -81,7 +81,7 @@ public class PKTaijiPuzzleValidator { return .failure(.invalidPetalCount(flower)) } } - + for region in 1...totalRegions { let diamonds = diamondConstraintsSatisfied(for: region) if !diamonds { @@ -94,46 +94,17 @@ public class PKTaijiPuzzleValidator { } } - if symbolLUT.slashdashes.count > 0 { - let regions = symbolLUT.slashdashes - .map { coordinate in - let regionID = self.regionMap[coordinate] ?? 1 - return self.regions[regionID] - } - let regionShapes = regions.enumerated().map { (index, regionDatum) in - regionDatum?.shape(relativeTo: symbolLUT.slashdashes[index]) ?? [] - } - - guard let expectedShape = regionShapes.first, - let baseTile = puzzle.tile(at: symbolLUT.slashdashes.first ?? .one) else { - return .failure(.regionShapeMapInvalid) - } - let expectedShapeRotates = baseTile.symbol == .slashdash(rotates: true) - for (index, shape) in regionShapes.dropFirst().enumerated() { - let sOrigin = symbolLUT.slashdashes[index+1] - guard let origin = puzzle.tile(at: sOrigin) else { continue } - - if expectedShapeRotates || origin.symbol == .slashdash(rotates: true) { - let result = Self.slashdashRotates( - lhs: expectedShape, - rhs: shape, - lhsOrigin: baseTile, - rhsOrigin: origin) - if !result { - return .failure(.invalidRegionShape) - } - } else { - let matches = Set(shape) == Set(expectedShape) - if !matches { - return .failure(.invalidRegionShape) - } - } - } + let response = validateSlashdashConstraints() + switch response { + case .success: + break + case .failure: + return response } - + return .success(()) } - + private func updateSymbolLUT(index: Int, tile: PKTaijiTile) { let coordinate = index.toCoordinate(wrappingAround: puzzle.width) switch tile.symbol { @@ -153,24 +124,24 @@ public class PKTaijiPuzzleValidator { return } } - + private func flowerConstraintsSatisfied(for coordinate: PKGridCoordinate) -> Bool { guard let tile = puzzle.tile(at: coordinate), case .flower(let petals) = tile.symbol else { return true } let flowerFilled = tile.filled - + let neighbors = [puzzle.tile(above: coordinate), puzzle.tile(before: coordinate), puzzle.tile(after: coordinate), puzzle.tile(below: coordinate)] - + let totalFilled = neighbors.count { tile in guard let tile else { return false } return tile.filled == flowerFilled } - + return totalFilled == petals } - + private func diamondConstraintsSatisfied(for region: Int) -> Bool { if options.contains(.ignoresColor) { let diamondsInRegion = symbolLUT.diamonds.filter { diamond in @@ -185,16 +156,16 @@ public class PKTaijiPuzzleValidator { return diamondRegion == region } - var colorMapping = [PKTaijiSymbolColor : Int]() + var colorMapping = [PKTaijiSymbolColor: Int]() for diamond in diamondsInRegion { guard let tile = puzzle.tile(at: diamond), let color = tile.color else { continue } colorMapping[color, default: 0] += 1 } - + if options.contains(.colorsMatchAnySymbols) { // TODO: Account for flowers and other symbols in this region with colors. } - + return colorMapping.allSatisfy { (_, count) in count == 2 || count == 0 } @@ -226,13 +197,52 @@ public class PKTaijiPuzzleValidator { return accum + value } + private func validateSlashdashConstraints() -> ValidationResult { + guard !symbolLUT.slashdashes.isEmpty else { return .success(()) } + let regions = symbolLUT.slashdashes + .map { coordinate in + let regionID = self.regionMap[coordinate] ?? 1 + return self.regions[regionID] + } + let regionShapes = regions.enumerated().map { (index, regionDatum) in + regionDatum?.shape(relativeTo: symbolLUT.slashdashes[index]) ?? [] + } + + guard let expectedShape = regionShapes.first, + let baseTile = puzzle.tile(at: symbolLUT.slashdashes.first ?? .one) else { + return .failure(.regionShapeMapInvalid) + } + let expectedShapeRotates = baseTile.symbol == .slashdash(rotates: true) + for (index, shape) in regionShapes.dropFirst().enumerated() { + let sOrigin = symbolLUT.slashdashes[index+1] + guard let origin = puzzle.tile(at: sOrigin) else { continue } + + if expectedShapeRotates || origin.symbol == .slashdash(rotates: true) { + let result = Self.slashdashRotates( + lhs: expectedShape, + rhs: shape, + lhsOrigin: baseTile, + rhsOrigin: origin) + if !result { + return .failure(.invalidRegionShape) + } + } else { + let matches = Set(shape) == Set(expectedShape) + if !matches { + return .failure(.invalidRegionShape) + } + } + } + return .success(()) + } + static func slashdashRotates( lhs: [PKGridCoordinate], rhs: [PKGridCoordinate], lhsOrigin: PKTaijiTile, rhsOrigin: PKTaijiTile ) -> Bool { - + guard case let .slashdash(lhsRotates) = lhsOrigin.symbol, case let .slashdash(rhsRotates) = rhsOrigin.symbol else { return false @@ -240,7 +250,7 @@ public class PKTaijiPuzzleValidator { var rotatingShape: [PKGridCoordinate] var staticShape: [PKGridCoordinate] - + switch (lhsRotates, rhsRotates) { case (true, false): rotatingShape = lhs