diff --git a/.github/workflows/commit-ci.yml b/.github/workflows/commit-ci.yml index e2bd4be61..828146722 100644 --- a/.github/workflows/commit-ci.yml +++ b/.github/workflows/commit-ci.yml @@ -3,6 +3,8 @@ on: push: branches: - '*' + paths: + - 'sources/**' jobs: build: runs-on: macos-14 @@ -12,6 +14,12 @@ jobs: with: submodules: true + - name: Install SwiftLint + run: brew install swiftlint + + - name: Lint + run: swiftlint + - name: Configure build environment run: | echo git_ref_name="$(git describe --always)" >> $GITHUB_ENV @@ -19,6 +27,12 @@ jobs: - name: Build Squirrel run: ./action-build.sh package + - name: Install periphery + run: brew install peripheryapp/periphery/periphery + + - name: Check Unused Code + run: periphery scan + - name: Upload Squirrel artifact uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 43200eb2c..408d07e88 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -9,6 +9,12 @@ jobs: with: submodules: true + - name: Install SwiftLint + run: brew install swiftlint + + - name: Lint + run: swiftlint + - name: Configure build environment run: | echo git_ref_name="$(git describe --always)" >> $GITHUB_ENV @@ -16,6 +22,12 @@ jobs: - name: Build Squirrel run: ./action-build.sh package + - name: Install periphery + run: brew install peripheryapp/periphery/periphery + + - name: Check Unused Code + run: periphery scan + - name: Upload Squirrel artifact uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/release-ci.yml b/.github/workflows/release-ci.yml index 3486eb631..044fb8637 100644 --- a/.github/workflows/release-ci.yml +++ b/.github/workflows/release-ci.yml @@ -5,6 +5,8 @@ on: - '*' branches: - master + paths: + - 'sources/**' workflow_dispatch: jobs: @@ -19,9 +21,21 @@ jobs: fetch-depth: 0 submodules: true + - name: Install SwiftLint + run: brew install swiftlint + + - name: Lint + run: swiftlint + - name: Build Squirrel run: ./action-build.sh archive + - name: Install periphery + run: brew install peripheryapp/periphery/periphery + + - name: Check Unused Code + run: periphery scan + - name: Build changelog id: release_log run: | diff --git a/.periphery.yml b/.periphery.yml new file mode 100644 index 000000000..a1842cba7 --- /dev/null +++ b/.periphery.yml @@ -0,0 +1,5 @@ +project: Squirrel.xcodeproj +schemes: +- Squirrel +targets: +- Squirrel diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 000000000..55bd033cb --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,56 @@ +# By default, SwiftLint uses a set of sensible default rules you can adjust: +disabled_rules: # rule identifiers turned on by default to exclude from running + - force_cast + - force_try + - todo +opt_in_rules: # some rules are turned off by default, so you need to opt-in + +# Alternatively, specify all rules explicitly by uncommenting this option: +# only_rules: # delete `disabled_rules` & `opt_in_rules` if using this +# - empty_parameters +# - vertical_whitespace + +analyzer_rules: # rules run by `swiftlint analyze` + - explicit_self + +included: # case-sensitive paths to include during linting. `--path` is ignored if present + - sources +excluded: # case-sensitive paths to ignore during linting. Takes precedence over `included` + +# If true, SwiftLint will not fail if no lintable files are found. +allow_zero_lintable_files: false + +# If true, SwiftLint will treat all warnings as errors. +strict: false + +# rules that have both warning and error levels, can set just the warning level +# implicitly +line_length: 200 +function_body_length: 200 +# they can set both implicitly with an array +type_body_length: + - 300 # warning + - 400 # error +# or they can set both explicitly +file_length: + warning: 800 + error: 1200 +# naming rules can set warnings/errors for min_length and max_length +# additionally they can set excluded names +type_name: + min_length: 4 # only warning + max_length: # warning and error + warning: 40 + error: 50 + excluded: # excluded via string + allowed_symbols: ["_"] # these are allowed in type names +identifier_name: + min_length: # only min_length + warning: 3 + error: 2 + excluded: [i, URL, of, by] # excluded via string array +large_tuple: + warning: 3 + warning: 5 +reporter: "github-actions-logging" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary) + diff --git a/sources/BridgingFunctions.swift b/sources/BridgingFunctions.swift index 7c3d40930..ed537b3e7 100644 --- a/sources/BridgingFunctions.swift +++ b/sources/BridgingFunctions.swift @@ -8,7 +8,8 @@ import Foundation protocol DataSizeable { - var data_size: Int32 { get set } + // swiftlint:disable:next identifier_name + var data_size: Int32 { get set } } extension RimeContext_stdbool: DataSizeable {} @@ -37,7 +38,7 @@ extension DataSizeable { let mutableCStr = strdup(cStr) // Free the existing string if there is one if let existing = self[keyPath: keypath] { - free(UnsafeMutableRawPointer(mutating: existing)) + free(UnsafeMutableRawPointer(mutating: existing)) } self[keyPath: keypath] = UnsafePointer(mutableCStr) } @@ -45,11 +46,13 @@ extension DataSizeable { } infix operator ?= : AssignmentPrecedence +// swiftlint:disable:next operator_whitespace func ?=(left: inout T, right: T?) { if let right = right { left = right } } +// swiftlint:disable:next operator_whitespace func ?=(left: inout T?, right: T?) { if let right = right { left = right diff --git a/sources/InputSource.swift b/sources/InputSource.swift index cf96eda89..a0c80add3 100644 --- a/sources/InputSource.swift +++ b/sources/InputSource.swift @@ -80,8 +80,8 @@ final class SquirrelInstaller { } for (mode, inputSource) in getInputSource(modes: [modeToSelect]) { if let enabled = getBool(for: inputSource, key: kTISPropertyInputSourceIsEnabled), - let selectable = getBool(for: inputSource, key: kTISPropertyInputSourceIsSelectCapable), - let selected = getBool(for: inputSource, key: kTISPropertyInputSourceIsSelected), + let selectable = getBool(for: inputSource, key: kTISPropertyInputSourceIsSelectCapable), + let selected = getBool(for: inputSource, key: kTISPropertyInputSourceIsSelected), enabled && selectable && !selected { let error = TISSelectInputSource(inputSource) print("Selection \(error == noErr ? "succeeds" : "fails") for input source: \(mode.rawValue)") diff --git a/sources/MacOSKeyCodes.swift b/sources/MacOSKeyCodes.swift index 5cf3ca5ac..ef99be00f 100644 --- a/sources/MacOSKeyCodes.swift +++ b/sources/MacOSKeyCodes.swift @@ -1,5 +1,5 @@ // -// MacOSKeyCOdes.swift +// MacOSKeyCodes.swift // Squirrel // // Created by Leo Liu on 5/9/24. diff --git a/sources/Main.swift b/sources/Main.swift index 005593681..3c54a7044 100644 --- a/sources/Main.swift +++ b/sources/Main.swift @@ -10,8 +10,8 @@ import InputMethodKit @main struct SquirrelApp { - static let userDir = if let pw = getpwuid(getuid()) { - URL(fileURLWithFileSystemRepresentation: pw.pointee.pw_dir, isDirectory: true, relativeTo: nil).appending(components: "Library", "Rime") + static let userDir = if let pwuid = getpwuid(getuid()) { + URL(fileURLWithFileSystemRepresentation: pwuid.pointee.pw_dir, isDirectory: true, relativeTo: nil).appending(components: "Library", "Rime") } else { try! FileManager.default.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Rime", isDirectory: true) } @@ -19,6 +19,7 @@ struct SquirrelApp { URL(fileURLWithFileSystemRepresentation: dir, isDirectory: false, relativeTo: nil) } + // swiftlint:disable:next cyclomatic_complexity static func main() { let rimeAPI: RimeApi_stdbool = rime_get_api_stdbool().pointee diff --git a/sources/SquirrelApplicationDelegate.swift b/sources/SquirrelApplicationDelegate.swift index ff674b725..a80d7e1d9 100644 --- a/sources/SquirrelApplicationDelegate.swift +++ b/sources/SquirrelApplicationDelegate.swift @@ -58,6 +58,7 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta } func applicationWillTerminate(_ notification: Notification) { + // swiftlint:disable:next notification_center_detachment NotificationCenter.default.removeObserver(self) DistributedNotificationCenter.default().removeObserver(self) panel?.hide() @@ -132,8 +133,10 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta print("Error creating user data directory: \(userDataDir.path())") } } + // swiftlint:disable identifier_name let notification_handler: @convention(c) (UnsafeMutableRawPointer?, RimeSessionId, UnsafePointer?, UnsafePointer?) -> Void = notificationHandler let context_object = Unmanaged.passUnretained(self).toOpaque() + // swiftlint:enable identifier_name rimeAPI.set_notification_handler(notification_handler, context_object) var squirrelTraits = RimeTraits.rimeStructInit() diff --git a/sources/SquirrelConfig.swift b/sources/SquirrelConfig.swift index 65d5d7abd..75b0eeb79 100644 --- a/sources/SquirrelConfig.swift +++ b/sources/SquirrelConfig.swift @@ -130,7 +130,7 @@ final class SquirrelConfig { // isVertical func updateTextOrientation(prefix: String) -> Bool { - let textOrientation = getString("\(prefix)/text_orientation") + let textOrientation = getString("\(prefix)/text_orientation") switch textOrientation { case "horizontal": return false diff --git a/sources/SquirrelInputController.swift b/sources/SquirrelInputController.swift index 6d47f16b8..df7aba759 100644 --- a/sources/SquirrelInputController.swift +++ b/sources/SquirrelInputController.swift @@ -28,6 +28,7 @@ final class SquirrelInputController: IMKInputController { private var chordDuration: TimeInterval = 0 private var currentApp: String = "" + // swiftlint:disable:next cyclomatic_complexity override func handle(_ event: NSEvent!, client sender: Any!) -> Bool { let modifiers = event.modifierFlags let changes = lastModifiers.symmetricDifference(modifiers) @@ -128,6 +129,7 @@ final class SquirrelInputController: IMKInputController { return success } + // swiftlint:disable:next identifier_name func page(up: Bool) -> Bool { var handled = false handled = rimeAPI.change_page(session, up) @@ -282,7 +284,8 @@ private extension SquirrelInputController { if chordKeyCount > 0 && session != 0 { // simulate key-ups for i in 0.. abs(scrollDirection.dy) && abs(scrollDirection.dx) > 10 { _ = inputController?.page(up: (scrollDirection.dx < 0) == vertical) @@ -106,7 +107,7 @@ final class SquirrelPanel: NSPanel { _ = inputController?.page(up: scrollDirection.dx > 0) } scrollDirection = .zero - // Mouse scroll wheel + // Mouse scroll wheel } else if event.phase == .init(rawValue: 0) && event.momentumPhase == .init(rawValue: 0) { if scrollTime.timeIntervalSinceNow < -1 { scrollDirection = .zero @@ -139,6 +140,7 @@ final class SquirrelPanel: NSPanel { } // Main function to add attributes to text output from librime + // swiftlint:disable:next cyclomatic_complexity function_parameter_count func update(preedit: String, selRange: NSRange, caretPos: Int, candidates: [String], comments: [String], labels: [String], highlighted index: Int, update: Bool) { if update { self.preedit = preedit @@ -335,6 +337,7 @@ private extension SquirrelPanel { // Get the window size, the windows will be the dirtyRect in // SquirrelView.drawRect + // swiftlint:disable:next cyclomatic_complexity func show() { currentScreen() let theme = view.currentTheme @@ -437,7 +440,8 @@ private extension SquirrelPanel { text.addAttribute(.paragraphStyle, value: theme.paragraphStyle, range: NSRange(location: 0, length: text.length)) view.textContentStorage.attributedString = text view.textView.setLayoutOrientation(vertical ? .vertical : .horizontal) - view.drawView(candidateRanges: [NSRange(location: 0, length: text.length)], hilightedIndex: -1, preeditRange: NSRange(location: NSNotFound, length: 0), highlightedPreeditRange: NSRange(location: NSNotFound, length: 0)) + view.drawView(candidateRanges: [NSRange(location: 0, length: text.length)], hilightedIndex: -1, + preeditRange: NSRange(location: NSNotFound, length: 0), highlightedPreeditRange: NSRange(location: NSNotFound, length: 0)) show() statusTimer?.invalidate() diff --git a/sources/SquirrelTheme.swift b/sources/SquirrelTheme.swift index 035903c6b..232f4e581 100644 --- a/sources/SquirrelTheme.swift +++ b/sources/SquirrelTheme.swift @@ -294,12 +294,12 @@ private extension SquirrelTheme { func blendColor(foregroundColor: NSColor, backgroundColor: NSColor?) -> NSColor { let foregroundColor = foregroundColor.usingColorSpace(NSColorSpace.deviceRGB)! let backgroundColor = (backgroundColor ?? NSColor.gray).usingColorSpace(NSColorSpace.deviceRGB)! - func blend(_ a: CGFloat, _ b: CGFloat) -> CGFloat { - return (a * 2 + b) / 3 + func blend(foreground: CGFloat, background: CGFloat) -> CGFloat { + return (foreground * 2 + background) / 3 } - return NSColor(deviceRed: blend(foregroundColor.redComponent, backgroundColor.redComponent), - green: blend(foregroundColor.greenComponent, backgroundColor.greenComponent), - blue: blend(foregroundColor.blueComponent, backgroundColor.blueComponent), - alpha: blend(foregroundColor.alphaComponent, backgroundColor.alphaComponent)) + return NSColor(deviceRed: blend(foreground: foregroundColor.redComponent, background: backgroundColor.redComponent), + green: blend(foreground: foregroundColor.greenComponent, background: backgroundColor.greenComponent), + blue: blend(foreground: foregroundColor.blueComponent, background: backgroundColor.blueComponent), + alpha: blend(foreground: foregroundColor.alphaComponent, background: backgroundColor.alphaComponent)) } } diff --git a/sources/SquirrelView.swift b/sources/SquirrelView.swift index 89ab93b83..e364ee359 100644 --- a/sources/SquirrelView.swift +++ b/sources/SquirrelView.swift @@ -10,7 +10,8 @@ import AppKit private class SquirrelLayoutDelegate: NSObject, NSTextLayoutManagerDelegate { func textLayoutManager(_ textLayoutManager: NSTextLayoutManager, shouldBreakLineBefore location: any NSTextLocation, hyphenating: Bool) -> Bool { let index = textLayoutManager.offset(from: textLayoutManager.documentRange.location, to: location) - if let attributes = textLayoutManager.textContainer?.textView?.textContentStorage?.attributedString?.attributes(at: index, effectiveRange: nil), let noBreak = attributes[.noBreak] as? Bool, noBreak { + if let attributes = textLayoutManager.textContainer?.textView?.textContentStorage?.attributedString?.attributes(at: index, effectiveRange: nil), + let noBreak = attributes[.noBreak] as? Bool, noBreak { return false } return true @@ -83,6 +84,7 @@ final class SquirrelView: NSView { if preeditRange.length > 0 { ranges.append(preeditRange) } + // swiftlint:disable:next identifier_name var x0 = CGFloat.infinity, x1 = -CGFloat.infinity, y0 = CGFloat.infinity, y1 = -CGFloat.infinity for range in ranges { if let textRange = convert(range: range) { @@ -97,6 +99,7 @@ final class SquirrelView: NSView { } // Get the rectangle containing the range of text, will first convert to glyph range, expensive to calculate func contentRect(range: NSTextRange) -> NSRect { + // swiftlint:disable:next identifier_name var x0 = CGFloat.infinity, x1 = -CGFloat.infinity, y0 = CGFloat.infinity, y1 = -CGFloat.infinity textLayoutManager.enumerateTextSegments(in: range, type: .standard, options: .rangeNotRequired) { _, rect, _, _ in x0 = min(rect.minX, x0) @@ -118,6 +121,7 @@ final class SquirrelView: NSView { } // All draws happen here + // swiftlint:disable:next cyclomatic_complexity override func draw(_ dirtyRect: NSRect) { var backgroundPath: CGPath? var preeditPath: CGPath? @@ -158,7 +162,8 @@ final class SquirrelView: NSView { } else { // Draw other highlighted Rect if candidate.length > 0 && theme.candidateBackColor != nil { - let candidatePath = drawPath(highlightedRange: candidate, backgroundRect: backgroundRect, preeditRect: preeditRect, containingRect: containingRect, extraExpansion: theme.surroundingExtraExpansion) + let candidatePath = drawPath(highlightedRange: candidate, backgroundRect: backgroundRect, preeditRect: preeditRect, + containingRect: containingRect, extraExpansion: theme.surroundingExtraExpansion) if candidatePaths == nil { candidatePaths = CGMutablePath() } @@ -413,6 +418,7 @@ private extension SquirrelView { } else if lineRects.count > 2 { leadingRect = lineRects[0] trailingRect = lineRects[lineRects.count-1] + // swiftlint:disable:next identifier_name var x0 = CGFloat.infinity, x1 = -CGFloat.infinity, y0 = CGFloat.infinity, y1 = -CGFloat.infinity for i in 1..<(lineRects.count-1) { let rect = lineRects[i]