feat: Add comprehensive Swift language detection support (#223)
* feat: Add comprehensive Swift language detection support Add Swift language detection with 40+ patterns covering syntax, stdlib, frameworks, and idioms. Implement fork-friendly architecture with separate swift_patterns.py module and graceful import fallback. Key changes: - New swift_patterns.py: 40+ Swift detection patterns (SwiftUI, Combine, async/await, property wrappers, etc.) - Enhanced language_detector.py: Graceful import handling, robust pattern compilation with error recovery - Comprehensive test suite: 19 tests covering syntax, frameworks, edge cases, and error handling - Updated .gitignore: Exclude Claude-specific config files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: Fix Swift pattern false positives and add comprehensive error handling Critical Fixes (Priority 0): - Fix 'some' and 'any' keyword false positives by requiring capitalized type names - Use (?-i:[A-Z]) to enforce case-sensitivity despite global IGNORECASE flag - Prevents "some random" from being detected as Swift code Error Handling (Priority 1): - Wrap pattern validation in try/except to prevent module import crashes - Add SWIFT_PATTERNS verification with logging after import - Gracefully degrade to empty dict on validation errors - Add 7 comprehensive error handling tests Improvements (Priority 2): - Remove fragile line number references in comments - Add 5 new tests for previously untested patterns: * Property observers (willSet/didSet) * Memory management (weak var, unowned, [weak self]) * String interpolation Test Results: - All 92 tests passing (72 Swift + 20 language detection) - Fixed regression: test_detect_unknown now passes - 12 new tests added (7 error handling + 5 feature coverage) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -56,3 +56,7 @@ htmlcov/
|
||||
# Build artifacts
|
||||
.build/
|
||||
skill-seekers-configs/
|
||||
.claude/skills
|
||||
.mcp.json
|
||||
settings.json
|
||||
USER_GUIDE.md
|
||||
|
||||
@@ -9,8 +9,45 @@ Author: Skill Seekers Project
|
||||
"""
|
||||
|
||||
import re
|
||||
import logging
|
||||
from typing import Optional, Tuple, Dict, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Import Swift patterns from separate module (fork-friendly architecture)
|
||||
try:
|
||||
from skill_seekers.cli.swift_patterns import SWIFT_PATTERNS
|
||||
except ImportError as e:
|
||||
logger.warning(
|
||||
"Swift language detection patterns unavailable. "
|
||||
"Swift code detection will be disabled. Error: %s",
|
||||
e
|
||||
)
|
||||
SWIFT_PATTERNS: Dict[str, List[Tuple[str, int]]] = {}
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to load Swift patterns due to unexpected error: %s. "
|
||||
"Swift detection disabled.",
|
||||
e
|
||||
)
|
||||
SWIFT_PATTERNS: Dict[str, List[Tuple[str, int]]] = {}
|
||||
|
||||
# Verify Swift patterns were loaded correctly
|
||||
if not SWIFT_PATTERNS:
|
||||
logger.warning(
|
||||
"Swift pattern dictionary is empty. Swift detection is disabled. "
|
||||
"This may indicate swift_patterns.py has no patterns defined."
|
||||
)
|
||||
elif 'swift' not in SWIFT_PATTERNS:
|
||||
logger.error(
|
||||
"Swift patterns loaded but 'swift' key is missing. "
|
||||
"Swift detection is broken. Please file a bug report."
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"Swift patterns loaded successfully: %d patterns for language detection",
|
||||
len(SWIFT_PATTERNS.get('swift', []))
|
||||
)
|
||||
|
||||
# Comprehensive language patterns with weighted confidence scoring
|
||||
# Weight 5: Unique identifiers (highly specific)
|
||||
@@ -371,6 +408,9 @@ LANGUAGE_PATTERNS: Dict[str, List[Tuple[str, int]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
# Merge Swift patterns (fork-friendly: patterns defined in swift_patterns.py)
|
||||
LANGUAGE_PATTERNS.update(SWIFT_PATTERNS)
|
||||
|
||||
|
||||
# Known language list for CSS class detection
|
||||
KNOWN_LANGUAGES = [
|
||||
@@ -418,10 +458,32 @@ class LanguageDetector:
|
||||
def _compile_patterns(self) -> None:
|
||||
"""Compile regex patterns and cache them for performance"""
|
||||
for lang, patterns in LANGUAGE_PATTERNS.items():
|
||||
self._pattern_cache[lang] = [
|
||||
(re.compile(pattern, re.IGNORECASE | re.MULTILINE), weight)
|
||||
for pattern, weight in patterns
|
||||
]
|
||||
compiled_patterns = []
|
||||
for i, (pattern, weight) in enumerate(patterns):
|
||||
try:
|
||||
compiled = re.compile(pattern, re.IGNORECASE | re.MULTILINE)
|
||||
compiled_patterns.append((compiled, weight))
|
||||
except re.error as e:
|
||||
logger.error(
|
||||
"Invalid regex pattern for language '%s' at index %d: '%s'. "
|
||||
"Error: %s. Pattern skipped.",
|
||||
lang, i, pattern[:50], e
|
||||
)
|
||||
except TypeError as e:
|
||||
logger.error(
|
||||
"Pattern for language '%s' at index %d is not a string: %s. "
|
||||
"Pattern skipped.",
|
||||
lang, i, type(pattern).__name__
|
||||
)
|
||||
|
||||
if compiled_patterns:
|
||||
self._pattern_cache[lang] = compiled_patterns
|
||||
else:
|
||||
logger.warning(
|
||||
"No valid patterns compiled for language '%s'. "
|
||||
"Detection for this language is disabled.",
|
||||
lang
|
||||
)
|
||||
|
||||
def detect_from_html(self, elem, code: str) -> Tuple[str, float]:
|
||||
"""
|
||||
|
||||
621
src/skill_seekers/cli/swift_patterns.py
Normal file
621
src/skill_seekers/cli/swift_patterns.py
Normal file
@@ -0,0 +1,621 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Swift Language Detection Patterns
|
||||
|
||||
Comprehensive regex patterns for detecting Swift code including:
|
||||
- Pure Swift syntax (structs, protocols, extensions, optionals, generics)
|
||||
- iOS/UIKit patterns (UIViewController, IBOutlet, lifecycle methods)
|
||||
- macOS/AppKit patterns (NSViewController, NSWindow, AppKit classes)
|
||||
- SwiftUI patterns (@State, @Binding, View protocol, modifiers)
|
||||
- Combine framework patterns (Publishers, Subscribers)
|
||||
- Swift Concurrency (async/await, actors, Tasks)
|
||||
- Foundation Models (iOS/macOS 26+: @Generable, LanguageModelSession, SystemLanguageModel)
|
||||
|
||||
Weight Scale:
|
||||
- 5: Unique to Swift (no other language uses this)
|
||||
- 4: Strong Swift indicator (rarely seen elsewhere)
|
||||
- 3: Common Swift pattern (moderate indicator)
|
||||
- 2: Moderate indicator (seen in some other languages)
|
||||
- 1: Weak indicator (commonly seen elsewhere)
|
||||
|
||||
Author: iLearn Project (Swift Support Extension)
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SWIFT_PATTERNS: Dict[str, List[Tuple[str, int]]] = {
|
||||
'swift': [
|
||||
# ===== PURE SWIFT SYNTAX (Weight 4-5) =====
|
||||
|
||||
# Function declarations with return type arrow (Swift-specific syntax)
|
||||
(r'\bfunc\s+\w+\s*\([^)]*\)\s*->', 5), # func foo() -> ReturnType
|
||||
(r'\bfunc\s+\w+\s*\([^)]*\)\s*\{', 4), # func foo() {
|
||||
|
||||
# Struct/class/enum declarations
|
||||
(r'\bstruct\s+\w+\s*:', 5), # struct Foo: Protocol
|
||||
(r'\bstruct\s+\w+\s*\{', 4), # struct Foo {
|
||||
(r'\bclass\s+\w+\s*:\s*\w+', 4), # class Foo: SuperClass
|
||||
(r'\benum\s+\w+\s*:\s*\w+', 4), # enum Foo: String
|
||||
(r'\benum\s+\w+\s*\{', 3), # enum Foo {
|
||||
|
||||
# Protocol declarations (weight 5 - Swift-specific in this context)
|
||||
(r'\bprotocol\s+\w+\s*\{', 5), # protocol Foo {
|
||||
(r'\bprotocol\s+\w+\s*:\s*\w+', 5), # protocol Foo: AnotherProtocol
|
||||
|
||||
# Extension (weight 5 - very Swift-specific syntax)
|
||||
(r'\bextension\s+\w+\s*:', 5), # extension Foo: Protocol
|
||||
(r'\bextension\s+\w+\s*\{', 5), # extension Foo {
|
||||
(r'\bextension\s+\w+\s*where\s+', 5), # extension Foo where T: Equatable
|
||||
|
||||
# Property wrappers (weight 5 - unique to Swift)
|
||||
(r'@\w+\s+var\s+\w+', 4), # @Something var foo
|
||||
(r'@propertyWrapper', 5), # @propertyWrapper attribute
|
||||
|
||||
# Optionals and unwrapping (weight 5 - unique to Swift)
|
||||
(r'\bguard\s+let\s+\w+\s*=', 5), # guard let foo =
|
||||
(r'\bguard\s+var\s+\w+\s*=', 5), # guard var foo =
|
||||
(r'\bif\s+let\s+\w+\s*=', 5), # if let foo =
|
||||
(r'\bif\s+var\s+\w+\s*=', 5), # if var foo =
|
||||
(r'\bguard\s+.*\s+else\s*\{', 4), # guard ... else {
|
||||
(r'\w+\?\.', 4), # foo?.bar (optional chaining)
|
||||
(r'\w+!\.\w+', 4), # foo!.bar (forced unwrap)
|
||||
(r'\?\?', 4), # nil coalescing ??
|
||||
|
||||
# Closures (weight 4-5 - Swift closure syntax)
|
||||
(r'\{\s*\([^)]*\)\s*in\b', 5), # { (params) in
|
||||
(r'\{\s*\$0', 4), # { $0 (shorthand argument)
|
||||
(r'\$[0-9]+', 3), # $0, $1, etc.
|
||||
|
||||
# Type annotations (weight 3-4)
|
||||
(r':\s*\[\w+\]', 4), # : [Type] (array type)
|
||||
(r':\s*\[\w+\s*:\s*\w+\]', 4), # : [Key: Value] (dictionary type)
|
||||
(r':\s*\w+\?', 4), # : Type? (optional type)
|
||||
(r'->\s*\w+\?', 4), # -> Type? (optional return)
|
||||
|
||||
# Keywords (weight 2-4)
|
||||
(r'\blet\s+\w+\s*:', 3), # let foo: Type
|
||||
(r'\bvar\s+\w+\s*:', 3), # var foo: Type
|
||||
(r'\blet\s+\w+\s*=', 2), # let foo = (also JS const)
|
||||
(r'\bself\.\w+', 3), # self.property (also Python)
|
||||
(r'\bSelf\b', 4), # Self (Swift-specific)
|
||||
|
||||
# Imports (weight 2-5 based on specificity)
|
||||
(r'\bimport\s+Foundation', 4), # import Foundation
|
||||
(r'\bimport\s+\w+', 2), # import Something
|
||||
|
||||
# Access modifiers (weight 3-5)
|
||||
(r'\bprivate\s*\(set\)', 5), # private(set) - Swift-specific
|
||||
(r'\bfileprivate\s+', 5), # fileprivate - Swift-only keyword
|
||||
(r'\binternal\s+', 2), # internal (also C#)
|
||||
(r'\bopen\s+class', 4), # open class (Swift-specific)
|
||||
|
||||
# Error handling (weight 4-5)
|
||||
(r'\bthrows\s*->', 5), # throws -> ReturnType
|
||||
(r'\bthrows\s*\{', 4), # func foo() throws {
|
||||
(r'\brethrows\b', 5), # rethrows keyword (Swift-only)
|
||||
(r'\btry\?\s+', 5), # try? (optional try - Swift)
|
||||
(r'\btry!\s+', 5), # try! (forced try - Swift)
|
||||
(r'\bdo\s*\{', 3), # do { (try block)
|
||||
(r'\bcatch\s+\w+', 3), # catch error
|
||||
|
||||
# Generics (weight 3-4)
|
||||
(r'<\w+:\s*\w+>', 4), # <T: Protocol>
|
||||
(r'\bwhere\s+\w+\s*:', 4), # where T: Protocol
|
||||
# NOTE: 'some' and 'any' require capitalized type names to avoid matching
|
||||
# English prose ("some example", "any variable"). Swift types are capitalized
|
||||
# by convention (View, Protocol, etc.). The (?-i:[A-Z]) syntax enforces
|
||||
# case-sensitive matching for the first letter despite global IGNORECASE flag.
|
||||
(r'\bsome\s+(?-i:[A-Z])\w+', 5), # some Protocol (opaque type - Swift 5.1+)
|
||||
(r'\bany\s+(?-i:[A-Z])\w+', 4), # any Protocol (existential type - Swift 5.6+)
|
||||
|
||||
# Actors and concurrency (weight 5 - Swift 5.5+)
|
||||
(r'\bactor\s+\w+', 5), # actor MyActor
|
||||
(r'\basync\s+throws', 5), # async throws (Swift-specific combo)
|
||||
(r'\basync\s+func', 5), # async func
|
||||
(r'\bawait\s+\w+', 3), # await (also JS/Python/C#)
|
||||
(r'\bTask\s*\{', 4), # Task { (Swift concurrency)
|
||||
(r'\bTask\.\w+', 4), # Task.detached, Task.sleep
|
||||
(r'@MainActor', 5), # @MainActor attribute
|
||||
(r'@Sendable', 5), # @Sendable attribute
|
||||
(r'\bnonisolated\b', 5), # nonisolated keyword
|
||||
|
||||
# Result builders (weight 5)
|
||||
(r'@resultBuilder', 5), # @resultBuilder attribute
|
||||
|
||||
# Type aliases and associated types (weight 5)
|
||||
(r'\btypealias\s+\w+\s*=', 5), # typealias Foo = Bar
|
||||
(r'\bassociatedtype\s+\w+', 5), # associatedtype Element
|
||||
|
||||
# Print function
|
||||
(r'\bprint\s*\(', 2), # print() (also Python)
|
||||
|
||||
# String interpolation (weight 4)
|
||||
(r'\\\(\w+', 4), # \(variable) interpolation
|
||||
|
||||
# Memory management (weight 4-5)
|
||||
(r'\bweak\s+var', 5), # weak var
|
||||
(r'\bunowned\s+', 5), # unowned (Swift-specific)
|
||||
(r'\[weak\s+self\]', 5), # [weak self] capture list
|
||||
(r'\[unowned\s+self\]', 5), # [unowned self] capture list
|
||||
(r'\blazy\s+var', 4), # lazy var
|
||||
|
||||
# ===== iOS/UIKit PATTERNS (Weight 4-5) =====
|
||||
|
||||
(r'\bimport\s+UIKit', 5), # import UIKit
|
||||
(r'\bUIViewController\b', 5), # UIViewController class
|
||||
(r'\bUIView\b', 4), # UIView class
|
||||
(r'\bUITableView\b', 5), # UITableView
|
||||
(r'\bUICollectionView\b', 5), # UICollectionView
|
||||
(r'\bUINavigationController\b', 5), # UINavigationController
|
||||
(r'\bUITabBarController\b', 5), # UITabBarController
|
||||
(r'\bUIButton\b', 4), # UIButton
|
||||
(r'\bUILabel\b', 4), # UILabel
|
||||
(r'\bUIImageView\b', 4), # UIImageView
|
||||
(r'\bUITextField\b', 4), # UITextField
|
||||
(r'\bUITextView\b', 4), # UITextView
|
||||
(r'\bUIStackView\b', 4), # UIStackView
|
||||
(r'\bUIScrollView\b', 4), # UIScrollView
|
||||
(r'\bUIAlertController\b', 5), # UIAlertController
|
||||
(r'\bUIApplication\b', 5), # UIApplication
|
||||
(r'\bUIWindow\b', 4), # UIWindow
|
||||
(r'\bUIScreen\b', 4), # UIScreen
|
||||
(r'\bUIDevice\b', 5), # UIDevice
|
||||
|
||||
# UIKit Lifecycle methods (weight 5)
|
||||
(r'\bviewDidLoad\s*\(\s*\)', 5), # viewDidLoad()
|
||||
(r'\bviewWillAppear\s*\(', 5), # viewWillAppear(_:)
|
||||
(r'\bviewDidAppear\s*\(', 5), # viewDidAppear(_:)
|
||||
(r'\bviewWillDisappear\s*\(', 5), # viewWillDisappear(_:)
|
||||
(r'\bviewDidDisappear\s*\(', 5), # viewDidDisappear(_:)
|
||||
(r'\bviewWillLayoutSubviews\s*\(\)', 5), # viewWillLayoutSubviews()
|
||||
(r'\bviewDidLayoutSubviews\s*\(\)', 5), # viewDidLayoutSubviews()
|
||||
|
||||
# Interface Builder outlets/actions (weight 5)
|
||||
(r'@IBOutlet', 5), # @IBOutlet
|
||||
(r'@IBAction', 5), # @IBAction
|
||||
(r'@IBDesignable', 5), # @IBDesignable
|
||||
(r'@IBInspectable', 5), # @IBInspectable
|
||||
|
||||
# UIKit delegates and datasources (weight 5)
|
||||
(r'\bUITableViewDelegate\b', 5),
|
||||
(r'\bUITableViewDataSource\b', 5),
|
||||
(r'\bUICollectionViewDelegate\b', 5),
|
||||
(r'\bUICollectionViewDataSource\b', 5),
|
||||
(r'\bUITextFieldDelegate\b', 5),
|
||||
(r'\bUIScrollViewDelegate\b', 5),
|
||||
|
||||
# Auto Layout (weight 4-5)
|
||||
(r'\bNSLayoutConstraint\b', 5), # NSLayoutConstraint
|
||||
(r'\.constraint\(', 4), # constraint methods
|
||||
(r'\btranslatesAutoresizingMaskIntoConstraints', 5),
|
||||
(r'NSLayoutConstraint\.activate', 5),
|
||||
|
||||
# GCD / DispatchQueue (weight 5)
|
||||
(r'\bDispatchQueue\b', 5), # DispatchQueue
|
||||
(r'\bDispatchQueue\.main', 5), # DispatchQueue.main
|
||||
(r'\bDispatchQueue\.global', 5), # DispatchQueue.global
|
||||
(r'\.async\s*\{', 4), # .async {
|
||||
(r'\.sync\s*\{', 4), # .sync {
|
||||
|
||||
# ===== macOS/AppKit PATTERNS (Weight 4-5) =====
|
||||
|
||||
(r'\bimport\s+AppKit', 5), # import AppKit
|
||||
(r'\bimport\s+Cocoa', 5), # import Cocoa
|
||||
(r'\bNSViewController\b', 5), # NSViewController
|
||||
(r'\bNSView\b', 4), # NSView
|
||||
(r'\bNSWindow\b', 5), # NSWindow
|
||||
(r'\bNSWindowController\b', 5), # NSWindowController
|
||||
(r'\bNSApplication\b', 5), # NSApplication
|
||||
(r'\bNSTableView\b', 5), # NSTableView
|
||||
(r'\bNSOutlineView\b', 5), # NSOutlineView
|
||||
(r'\bNSCollectionView\b', 5), # NSCollectionView
|
||||
(r'\bNSButton\b', 4), # NSButton
|
||||
(r'\bNSTextField\b', 4), # NSTextField
|
||||
(r'\bNSTextView\b', 4), # NSTextView
|
||||
(r'\bNSImageView\b', 4), # NSImageView
|
||||
(r'\bNSStackView\b', 4), # NSStackView
|
||||
(r'\bNSScrollView\b', 4), # NSScrollView
|
||||
(r'\bNSSplitView\b', 5), # NSSplitView
|
||||
(r'\bNSTabView\b', 5), # NSTabView
|
||||
(r'\bNSMenu\b', 5), # NSMenu
|
||||
(r'\bNSMenuItem\b', 5), # NSMenuItem
|
||||
(r'\bNSToolbar\b', 5), # NSToolbar
|
||||
(r'\bNSAlert\b', 5), # NSAlert
|
||||
(r'\bNSPanel\b', 5), # NSPanel
|
||||
(r'\bNSOpenPanel\b', 5), # NSOpenPanel
|
||||
(r'\bNSSavePanel\b', 5), # NSSavePanel
|
||||
(r'\bNSWorkspace\b', 5), # NSWorkspace
|
||||
(r'\bNSRunningApplication\b', 5), # NSRunningApplication
|
||||
(r'\bNSScreen\b', 4), # NSScreen
|
||||
(r'\bNSColor\b', 4), # NSColor
|
||||
(r'\bNSFont\b', 4), # NSFont
|
||||
(r'\bNSImage\b', 4), # NSImage
|
||||
(r'\bNSBezierPath\b', 5), # NSBezierPath
|
||||
(r'\bNSSound\b', 5), # NSSound
|
||||
(r'\bNSEvent\b', 5), # NSEvent
|
||||
(r'\bNSResponder\b', 5), # NSResponder
|
||||
(r'\bNSPasteboard\b', 5), # NSPasteboard
|
||||
(r'\bNSStatusBar\b', 5), # NSStatusBar
|
||||
(r'\bNSStatusItem\b', 5), # NSStatusItem
|
||||
|
||||
# macOS Lifecycle methods (weight 5)
|
||||
# Note: viewDidLoad() is defined in the UIKit section above since it's shared
|
||||
# between iOS (UIViewController) and macOS (NSViewController)
|
||||
(r'\bviewWillAppear\s*\(\)', 5), # NSViewController viewWillAppear
|
||||
(r'\bviewDidAppear\s*\(\)', 5), # NSViewController viewDidAppear
|
||||
(r'\bawakeFromNib\s*\(\)', 5), # awakeFromNib()
|
||||
(r'\bapplicationDidFinishLaunching', 5), # NSApplicationDelegate
|
||||
(r'\bapplicationWillTerminate', 5), # NSApplicationDelegate
|
||||
(r'\bwindowDidLoad\s*\(\)', 5), # NSWindowController
|
||||
|
||||
# macOS delegates (weight 5)
|
||||
(r'\bNSTableViewDelegate\b', 5),
|
||||
(r'\bNSTableViewDataSource\b', 5),
|
||||
(r'\bNSOutlineViewDelegate\b', 5),
|
||||
(r'\bNSOutlineViewDataSource\b', 5),
|
||||
(r'\bNSWindowDelegate\b', 5),
|
||||
(r'\bNSApplicationDelegate\b', 5),
|
||||
(r'\bNSTextFieldDelegate\b', 5),
|
||||
(r'\bNSTextViewDelegate\b', 5),
|
||||
|
||||
# ===== SwiftUI PATTERNS (Weight 5) =====
|
||||
|
||||
(r'\bimport\s+SwiftUI', 5), # import SwiftUI
|
||||
(r'\bstruct\s+\w+\s*:\s*View', 5), # struct Foo: View
|
||||
(r'\bvar\s+body\s*:\s*some\s+View', 5), # var body: some View
|
||||
(r':\s*some\s+View', 5), # : some View
|
||||
|
||||
# SwiftUI property wrappers (weight 5 - unique to SwiftUI)
|
||||
(r'@State\s+', 5), # @State var
|
||||
(r'@Binding\s+', 5), # @Binding var
|
||||
(r'@Published\s+', 5), # @Published var
|
||||
(r'@ObservedObject\s+', 5), # @ObservedObject var
|
||||
(r'@StateObject\s+', 5), # @StateObject var
|
||||
(r'@EnvironmentObject\s+', 5), # @EnvironmentObject var
|
||||
(r'@Environment\s*\(', 5), # @Environment(\.keyPath)
|
||||
(r'@FetchRequest\s*\(', 5), # @FetchRequest (Core Data)
|
||||
(r'@AppStorage\s*\(', 5), # @AppStorage
|
||||
(r'@SceneStorage\s*\(', 5), # @SceneStorage
|
||||
(r'@FocusState\s+', 5), # @FocusState
|
||||
(r'@FocusedBinding\s*\(', 5), # @FocusedBinding
|
||||
(r'@Observable\b', 5), # @Observable (Swift 5.9+)
|
||||
(r'@Bindable\s+', 5), # @Bindable (Swift 5.9+)
|
||||
(r'@Query\s*\(', 5), # @Query (SwiftData)
|
||||
(r'@Model\b', 5), # @Model (SwiftData)
|
||||
(r'@ViewBuilder', 5), # @ViewBuilder
|
||||
|
||||
# SwiftUI Views (weight 4-5)
|
||||
(r'\bText\s*\(', 4), # Text("Hello")
|
||||
(r'\bImage\s*\(', 3), # Image(systemName:)
|
||||
(r'\bButton\s*\(', 3), # Button("Label") { }
|
||||
(r'\bVStack\s*[\(\{]', 5), # VStack { } or VStack(alignment:)
|
||||
(r'\bHStack\s*[\(\{]', 5), # HStack { }
|
||||
(r'\bZStack\s*[\(\{]', 5), # ZStack { }
|
||||
(r'\bList\s*[\(\{]', 4), # List { }
|
||||
(r'\bForEach\s*\(', 4), # ForEach(items) { }
|
||||
(r'\bNavigationView\s*\{', 5), # NavigationView { }
|
||||
(r'\bNavigationStack\s*[\(\{]', 5), # NavigationStack { } (iOS 16+)
|
||||
(r'\bNavigationSplitView\s*[\(\{]', 5), # NavigationSplitView (macOS/iPad)
|
||||
(r'\bNavigationLink\s*\(', 5), # NavigationLink
|
||||
(r'\bTabView\s*[\(\{]', 5), # TabView { }
|
||||
(r'\bScrollView\s*[\(\{]', 5), # ScrollView { }
|
||||
(r'\bLazyVStack\s*[\(\{]', 5), # LazyVStack { }
|
||||
(r'\bLazyHStack\s*[\(\{]', 5), # LazyHStack { }
|
||||
(r'\bLazyVGrid\s*\(', 5), # LazyVGrid
|
||||
(r'\bLazyHGrid\s*\(', 5), # LazyHGrid
|
||||
(r'\bGrid\s*[\(\{]', 4), # Grid { } (iOS 16+)
|
||||
(r'\bGridRow\s*[\(\{]', 5), # GridRow { }
|
||||
(r'\bGeometryReader\s*\{', 5), # GeometryReader { }
|
||||
(r'\bSpacer\s*\(\)', 5), # Spacer()
|
||||
(r'\bDivider\s*\(\)', 5), # Divider()
|
||||
(r'\bForm\s*\{', 4), # Form { }
|
||||
(r'\bSection\s*[\(\{]', 4), # Section { }
|
||||
(r'\bGroup\s*\{', 4), # Group { }
|
||||
(r'\bGroupBox\s*[\(\{]', 5), # GroupBox { }
|
||||
(r'\bDisclosureGroup\s*\(', 5), # DisclosureGroup
|
||||
(r'\bOutlineGroup\s*\(', 5), # OutlineGroup
|
||||
(r'\bToggle\s*\(', 4), # Toggle
|
||||
(r'\bPicker\s*\(', 4), # Picker
|
||||
(r'\bSlider\s*\(', 4), # Slider
|
||||
(r'\bStepper\s*\(', 4), # Stepper
|
||||
(r'\bDatePicker\s*\(', 5), # DatePicker
|
||||
(r'\bColorPicker\s*\(', 5), # ColorPicker
|
||||
(r'\bProgressView\s*[\(\{]', 5), # ProgressView
|
||||
(r'\bLabel\s*\(', 4), # Label
|
||||
(r'\bLink\s*\(', 4), # Link
|
||||
(r'\bMenu\s*[\(\{]', 4), # Menu
|
||||
(r'\bContextMenu\s*\{', 5), # ContextMenu
|
||||
(r'\bToolbar\s*\{', 5), # Toolbar
|
||||
(r'\bToolbarItem\s*\(', 5), # ToolbarItem
|
||||
(r'\bCanvas\s*\{', 5), # Canvas
|
||||
(r'\bTimelineView\s*\(', 5), # TimelineView
|
||||
(r'\bShareLink\s*\(', 5), # ShareLink (iOS 16+)
|
||||
(r'\bPhotosPicker\s*\(', 5), # PhotosPicker
|
||||
(r'\bTextField\s*\(', 4), # TextField
|
||||
(r'\bSecureField\s*\(', 5), # SecureField
|
||||
(r'\bTextEditor\s*\(', 5), # TextEditor
|
||||
|
||||
# SwiftUI Modifiers (weight 4-5)
|
||||
(r'\.padding\s*\(', 4), # .padding()
|
||||
(r'\.frame\s*\(', 4), # .frame(width:height:)
|
||||
(r'\.foregroundColor\s*\(', 5), # .foregroundColor(.red)
|
||||
(r'\.foregroundStyle\s*\(', 5), # .foregroundStyle (iOS 15+)
|
||||
(r'\.background\s*\(', 3), # .background()
|
||||
(r'\.cornerRadius\s*\(', 4), # .cornerRadius()
|
||||
(r'\.clipShape\s*\(', 5), # .clipShape()
|
||||
(r'\.shadow\s*\(', 3), # .shadow()
|
||||
(r'\.font\s*\(', 3), # .font(.title)
|
||||
(r'\.fontWeight\s*\(', 4), # .fontWeight()
|
||||
(r'\.bold\s*\(\)', 4), # .bold()
|
||||
(r'\.italic\s*\(\)', 4), # .italic()
|
||||
(r'\.onAppear\s*\{', 5), # .onAppear { }
|
||||
(r'\.onDisappear\s*\{', 5), # .onDisappear { }
|
||||
(r'\.onTapGesture\s*\{', 5), # .onTapGesture { }
|
||||
(r'\.gesture\s*\(', 4), # .gesture()
|
||||
(r'\.sheet\s*\(', 5), # .sheet(isPresented:)
|
||||
(r'\.fullScreenCover\s*\(', 5), # .fullScreenCover()
|
||||
(r'\.popover\s*\(', 5), # .popover()
|
||||
(r'\.alert\s*\(', 4), # .alert()
|
||||
(r'\.confirmationDialog\s*\(', 5), # .confirmationDialog()
|
||||
(r'\.navigationTitle\s*\(', 5), # .navigationTitle()
|
||||
(r'\.navigationBarTitleDisplayMode', 5), # .navigationBarTitleDisplayMode
|
||||
(r'\.toolbar\s*\{', 5), # .toolbar { }
|
||||
(r'\.toolbarBackground\s*\(', 5), # .toolbarBackground()
|
||||
(r'\.environmentObject\s*\(', 5), # .environmentObject()
|
||||
(r'\.environment\s*\(', 4), # .environment()
|
||||
(r'\.task\s*\{', 5), # .task { } (async)
|
||||
(r'\.refreshable\s*\{', 5), # .refreshable { }
|
||||
(r'\.searchable\s*\(', 5), # .searchable()
|
||||
(r'\.onChange\s*\(', 5), # .onChange(of:)
|
||||
(r'\.onSubmit\s*\{', 5), # .onSubmit { }
|
||||
(r'\.focused\s*\(', 5), # .focused()
|
||||
(r'\.disabled\s*\(', 4), # .disabled()
|
||||
(r'\.opacity\s*\(', 3), # .opacity()
|
||||
(r'\.offset\s*\(', 4), # .offset()
|
||||
(r'\.rotationEffect\s*\(', 5), # .rotationEffect()
|
||||
(r'\.scaleEffect\s*\(', 5), # .scaleEffect()
|
||||
(r'\.animation\s*\(', 4), # .animation()
|
||||
(r'\.transition\s*\(', 5), # .transition()
|
||||
(r'\.withAnimation\s*\{', 5), # withAnimation { }
|
||||
(r'\.matchedGeometryEffect\s*\(', 5), # .matchedGeometryEffect()
|
||||
(r'\.contentShape\s*\(', 5), # .contentShape()
|
||||
(r'\.allowsHitTesting\s*\(', 5), # .allowsHitTesting()
|
||||
(r'\.overlay\s*\(', 4), # .overlay()
|
||||
(r'\.mask\s*\(', 4), # .mask()
|
||||
(r'\.zIndex\s*\(', 4), # .zIndex()
|
||||
(r'\.layoutPriority\s*\(', 5), # .layoutPriority()
|
||||
(r'\.preference\s*\(', 5), # .preference()
|
||||
(r'\.onPreferenceChange\s*\(', 5), # .onPreferenceChange()
|
||||
(r'\.coordinateSpace\s*\(', 5), # .coordinateSpace()
|
||||
(r'\.ignoresSafeArea\s*\(', 5), # .ignoresSafeArea()
|
||||
(r'\.safeAreaInset\s*\(', 5), # .safeAreaInset()
|
||||
(r'\.listStyle\s*\(', 5), # .listStyle()
|
||||
(r'\.buttonStyle\s*\(', 5), # .buttonStyle()
|
||||
(r'\.textFieldStyle\s*\(', 5), # .textFieldStyle()
|
||||
(r'\.pickerStyle\s*\(', 5), # .pickerStyle()
|
||||
(r'\.labelStyle\s*\(', 5), # .labelStyle()
|
||||
(r'\.toggleStyle\s*\(', 5), # .toggleStyle()
|
||||
(r'\.presentationDetents\s*\(', 5), # .presentationDetents() (iOS 16+)
|
||||
(r'\.interactiveDismissDisabled\s*\(', 5), # .interactiveDismissDisabled()
|
||||
|
||||
# SwiftUI Scene types (macOS/multi-window)
|
||||
(r'\bWindowGroup\s*\{', 5), # WindowGroup { }
|
||||
(r'\bWindow\s*\(', 5), # Window (macOS)
|
||||
(r'\bSettings\s*\{', 5), # Settings { } (macOS)
|
||||
(r'\bMenuBarExtra\s*\(', 5), # MenuBarExtra (macOS)
|
||||
(r'\bDocumentGroup\s*\(', 5), # DocumentGroup
|
||||
(r':\s*App\s*\{', 5), # : App {
|
||||
(r'@main\b', 5), # @main
|
||||
(r'var\s+body:\s*some\s+Scene', 5), # var body: some Scene
|
||||
|
||||
# ===== Combine Framework (Weight 5) =====
|
||||
|
||||
(r'\bimport\s+Combine', 5), # import Combine
|
||||
(r'\bAnyPublisher\b', 5), # AnyPublisher
|
||||
(r'\bPassthroughSubject\b', 5), # PassthroughSubject
|
||||
(r'\bCurrentValueSubject\b', 5), # CurrentValueSubject
|
||||
(r'\bPublisher\b', 4), # Publisher protocol
|
||||
(r'\bSubscriber\b', 4), # Subscriber protocol
|
||||
(r'\.sink\s*\{', 5), # .sink { }
|
||||
(r'\.receive\s*\(on:\s*', 5), # .receive(on: RunLoop.main)
|
||||
(r'\bAnyCancellable\b', 5), # AnyCancellable
|
||||
(r'\.store\s*\(in:\s*&', 5), # .store(in: &cancellables)
|
||||
(r'\.eraseToAnyPublisher\s*\(\)', 5), # .eraseToAnyPublisher()
|
||||
(r'\.map\s*\{\s*\$0', 5), # .map { $0 (Combine map)
|
||||
(r'\.flatMap\s*\{', 4), # .flatMap {
|
||||
(r'\.compactMap\s*\{', 4), # .compactMap {
|
||||
(r'\.filter\s*\{', 3), # .filter {
|
||||
(r'\.debounce\s*\(', 5), # .debounce()
|
||||
(r'\.throttle\s*\(', 5), # .throttle()
|
||||
(r'\.removeDuplicates\s*\(', 5), # .removeDuplicates()
|
||||
(r'\.combineLatest\s*\(', 5), # .combineLatest()
|
||||
(r'\.merge\s*\(', 4), # .merge()
|
||||
(r'\.zip\s*\(', 3), # .zip()
|
||||
(r'@Published\s+var', 5), # @Published var
|
||||
|
||||
# ===== Codable/JSON (Weight 5) =====
|
||||
|
||||
(r'\bCodable\b', 5), # Codable protocol
|
||||
(r'\bEncodable\b', 4), # Encodable protocol
|
||||
(r'\bDecodable\b', 4), # Decodable protocol
|
||||
(r'\bJSONDecoder\s*\(\)', 5), # JSONDecoder()
|
||||
(r'\bJSONEncoder\s*\(\)', 5), # JSONEncoder()
|
||||
(r'\bCodingKeys\b', 5), # CodingKeys enum
|
||||
(r'\bPropertyListDecoder\b', 5), # PropertyListDecoder
|
||||
(r'\bPropertyListEncoder\b', 5), # PropertyListEncoder
|
||||
|
||||
# ===== Core Data (Weight 5) =====
|
||||
|
||||
(r'\bimport\s+CoreData', 5), # import CoreData
|
||||
(r'\bNSManagedObject\b', 5), # NSManagedObject
|
||||
(r'\bNSManagedObjectContext\b', 5), # NSManagedObjectContext
|
||||
(r'\bNSPersistentContainer\b', 5), # NSPersistentContainer
|
||||
(r'\bNSFetchRequest\b', 5), # NSFetchRequest
|
||||
(r'\b@FetchRequest\b', 5), # @FetchRequest property wrapper
|
||||
(r'\bNSPredicate\b', 5), # NSPredicate
|
||||
(r'\bNSSortDescriptor\b', 5), # NSSortDescriptor
|
||||
|
||||
# ===== SwiftData (Weight 5 - iOS 17+) =====
|
||||
|
||||
(r'\bimport\s+SwiftData', 5), # import SwiftData
|
||||
(r'@Model\s+', 5), # @Model class
|
||||
(r'@Attribute\s*\(', 5), # @Attribute
|
||||
(r'@Relationship\s*\(', 5), # @Relationship
|
||||
(r'\bModelContext\b', 5), # ModelContext
|
||||
(r'\bModelContainer\b', 5), # ModelContainer
|
||||
|
||||
# ===== Common Apple Frameworks (Weight 4-5) =====
|
||||
|
||||
(r'\bimport\s+MapKit', 5), # import MapKit
|
||||
(r'\bimport\s+CoreLocation', 5), # import CoreLocation
|
||||
(r'\bimport\s+AVFoundation', 5), # import AVFoundation
|
||||
(r'\bimport\s+Photos', 5), # import Photos
|
||||
(r'\bimport\s+PhotosUI', 5), # import PhotosUI
|
||||
(r'\bimport\s+HealthKit', 5), # import HealthKit
|
||||
(r'\bimport\s+StoreKit', 5), # import StoreKit
|
||||
(r'\bimport\s+CloudKit', 5), # import CloudKit
|
||||
(r'\bimport\s+UserNotifications', 5), # import UserNotifications
|
||||
(r'\bimport\s+EventKit', 5), # import EventKit
|
||||
(r'\bimport\s+Contacts', 5), # import Contacts
|
||||
(r'\bimport\s+MessageUI', 5), # import MessageUI
|
||||
(r'\bimport\s+SafariServices', 5), # import SafariServices
|
||||
(r'\bimport\s+WebKit', 5), # import WebKit
|
||||
(r'\bimport\s+PDFKit', 5), # import PDFKit
|
||||
(r'\bimport\s+QuickLook', 5), # import QuickLook
|
||||
(r'\bimport\s+AuthenticationServices', 5), # import AuthenticationServices
|
||||
(r'\bimport\s+LocalAuthentication', 5), # import LocalAuthentication
|
||||
(r'\bimport\s+GameKit', 5), # import GameKit
|
||||
(r'\bimport\s+SpriteKit', 5), # import SpriteKit
|
||||
(r'\bimport\s+SceneKit', 5), # import SceneKit
|
||||
(r'\bimport\s+RealityKit', 5), # import RealityKit
|
||||
(r'\bimport\s+ARKit', 5), # import ARKit
|
||||
(r'\bimport\s+Metal', 5), # import Metal
|
||||
(r'\bimport\s+CoreML', 5), # import CoreML
|
||||
(r'\bimport\s+Vision', 5), # import Vision
|
||||
(r'\bimport\s+NaturalLanguage', 5), # import NaturalLanguage
|
||||
(r'\bimport\s+Speech', 5), # import Speech
|
||||
(r'\bimport\s+CoreBluetooth', 5), # import CoreBluetooth
|
||||
(r'\bimport\s+NetworkExtension', 5), # import NetworkExtension
|
||||
(r'\bimport\s+WidgetKit', 5), # import WidgetKit
|
||||
(r'\bimport\s+ActivityKit', 5), # import ActivityKit
|
||||
(r'\bimport\s+AppIntents', 5), # import AppIntents
|
||||
|
||||
# ===== Foundation Models Framework (iOS/macOS/visionOS 26+) =====
|
||||
# Apple's on-device AI/ML framework for language model interactions
|
||||
|
||||
# Import statement
|
||||
(r'\bimport\s+FoundationModels', 5), # import FoundationModels
|
||||
|
||||
# Core classes
|
||||
(r'\bSystemLanguageModel\b', 5), # Main model class
|
||||
(r'\bLanguageModelSession\b', 5), # Session for interactions
|
||||
(r'\bLanguageModelFeedback\b', 5), # Feedback reporting
|
||||
|
||||
# Key structs
|
||||
(r'\bInstructionsBuilder\b', 5), # Result builder for instructions
|
||||
(r'\bPromptBuilder\b', 5), # Result builder for prompts
|
||||
(r'\bGenerationOptions\b', 5), # Controls generation behavior
|
||||
(r'\bGeneratedContent\b', 5), # Structured output type
|
||||
(r'\bGenerationID\b', 5), # Unique generation identifier
|
||||
(r'\bGenerationSchema\b', 5), # Schema for guided generation
|
||||
(r'\bDynamicGenerationSchema\b', 5), # Runtime schema definition
|
||||
(r'\bGenerationGuide\b', 5), # Value constraint guides
|
||||
|
||||
# Macros (unique to FoundationModels)
|
||||
(r'@Generable\b', 5), # Guided generation macro
|
||||
(r'@Generable\s*\(\s*description:', 5), # @Generable with description
|
||||
(r'@Guide\b', 5), # Property constraint macro
|
||||
(r'@Guide\s*\(\s*description:', 5), # @Guide with description
|
||||
|
||||
# Key protocols
|
||||
(r'\bGenerable\b', 4), # Core protocol (also common word)
|
||||
(r'\bInstructionsRepresentable\b', 5), # Instructions protocol
|
||||
(r'\bPromptRepresentable\b', 5), # Prompt protocol
|
||||
(r'\bConvertibleFromGeneratedContent\b', 5), # Content conversion
|
||||
(r'\bConvertibleToGeneratedContent\b', 5), # Content conversion
|
||||
|
||||
# Nested types (SystemLanguageModel.*)
|
||||
(r'\bSystemLanguageModel\.default\b', 5), # Default model access
|
||||
(r'\bSystemLanguageModel\.UseCase\b', 5), # Use case type
|
||||
(r'\bSystemLanguageModel\.Guardrails\b', 5), # Safety guardrails
|
||||
(r'\bSystemLanguageModel\.Adapter\b', 5), # Custom adapters
|
||||
(r'\bSystemLanguageModel\.Availability\b', 5), # Availability enum
|
||||
|
||||
# Key methods
|
||||
(r'\.respond\s*\(to:', 4), # Primary response method
|
||||
(r'\.respond\s*\([^)]*generating:', 5), # Guided generation response
|
||||
(r'\.streamResponse\s*\(', 4), # Streaming response
|
||||
(r'\.prewarm\s*\(', 5), # Session prewarming
|
||||
(r'\.logFeedbackAttachment\s*\(', 5), # Feedback logging
|
||||
|
||||
# Transcript types
|
||||
(r'\bTranscript\.Entry\b', 5), # Transcript entry
|
||||
(r'\bTranscript\.Segment\b', 5), # Transcript segment
|
||||
(r'\bTranscript\.ToolCall\b', 5), # Tool call record
|
||||
(r'\bTranscript\.ToolOutput\b', 5), # Tool output record
|
||||
(r'\bTranscript\.Response\b', 5), # Response record
|
||||
|
||||
# Error types and availability
|
||||
(r'\bGenerationError\b', 4), # Generation error type
|
||||
(r'\bToolCallError\b', 5), # Tool execution error
|
||||
(r'\.exceededContextWindowSize\b', 5), # Context window error
|
||||
(r'\.guardrailViolation\b', 5), # Safety guardrail error
|
||||
(r'\.appleIntelligenceNotEnabled\b', 5), # Availability reason
|
||||
(r'\.deviceNotEligible\b', 5), # Device eligibility
|
||||
(r'\.modelNotReady\b', 5), # Model readiness
|
||||
|
||||
# Use cases and guardrails
|
||||
(r'\.contentTagging\b', 5), # Content tagging use case
|
||||
(r'\.permissiveContentTransformations\b', 5), # Guardrail setting
|
||||
|
||||
# Common usage patterns
|
||||
(r'LanguageModelSession\s*\(\s*instructions:', 5), # Session init
|
||||
(r'for\s+try\s+await.*streamResponse', 5), # Streaming iteration
|
||||
(r'\.PartiallyGenerated\b', 5), # Partial generation type
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _validate_patterns(patterns: Dict[str, List[Tuple[str, int]]]) -> None:
|
||||
"""
|
||||
Validate pattern structure at module load time.
|
||||
|
||||
Ensures all patterns follow the expected format:
|
||||
- Each pattern is a (regex_string, weight) tuple
|
||||
- Weight is an integer between 1 and 5
|
||||
- Regex string is a valid string
|
||||
|
||||
Raises:
|
||||
ValueError: If any pattern is malformed
|
||||
"""
|
||||
for lang, pattern_list in patterns.items():
|
||||
for i, item in enumerate(pattern_list):
|
||||
if not isinstance(item, tuple) or len(item) != 2:
|
||||
raise ValueError(
|
||||
f"Pattern {i} for '{lang}' is not a (regex, weight) tuple: {item}"
|
||||
)
|
||||
pattern, weight = item
|
||||
if not isinstance(pattern, str):
|
||||
raise ValueError(
|
||||
f"Pattern {i} for '{lang}': regex must be a string, got {type(pattern).__name__}"
|
||||
)
|
||||
if not isinstance(weight, int) or weight < 1 or weight > 5:
|
||||
raise ValueError(
|
||||
f"Pattern {i} for '{lang}': weight must be int 1-5, got {weight!r}"
|
||||
)
|
||||
|
||||
|
||||
# Validate patterns at module load time
|
||||
try:
|
||||
_validate_patterns(SWIFT_PATTERNS)
|
||||
except ValueError as e:
|
||||
logger.error(
|
||||
"Swift pattern validation failed: %s. Swift detection will be disabled. "
|
||||
"This indicates a bug in swift_patterns.py - please file an issue.",
|
||||
e
|
||||
)
|
||||
# Clear patterns to prevent broken detection with invalid data
|
||||
SWIFT_PATTERNS = {}
|
||||
1397
tests/test_swift_detection.py
Normal file
1397
tests/test_swift_detection.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user