diff --git a/.gitignore b/.gitignore index 85d5f46..ae76439 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,7 @@ htmlcov/ # Build artifacts .build/ skill-seekers-configs/ +.claude/skills +.mcp.json +settings.json +USER_GUIDE.md diff --git a/src/skill_seekers/cli/language_detector.py b/src/skill_seekers/cli/language_detector.py index 928a1fd..2992c55 100644 --- a/src/skill_seekers/cli/language_detector.py +++ b/src/skill_seekers/cli/language_detector.py @@ -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]: """ diff --git a/src/skill_seekers/cli/swift_patterns.py b/src/skill_seekers/cli/swift_patterns.py new file mode 100644 index 0000000..7b65318 --- /dev/null +++ b/src/skill_seekers/cli/swift_patterns.py @@ -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), # + (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 = {} diff --git a/tests/test_swift_detection.py b/tests/test_swift_detection.py new file mode 100644 index 0000000..3c4a252 --- /dev/null +++ b/tests/test_swift_detection.py @@ -0,0 +1,1397 @@ +#!/usr/bin/env python3 +""" +Test Suite for Swift Language Detection + +Tests confidence-based Swift detection 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 +- Swift Concurrency (async/await, actors) +- Foundation Models (iOS/macOS 26+: @Generable, LanguageModelSession, SystemLanguageModel) + +Run with: pytest tests/test_swift_detection.py -v +""" + +import pytest +from bs4 import BeautifulSoup +from skill_seekers.cli.language_detector import LanguageDetector + + +class TestSwiftCSSClassDetection: + """Test Swift detection from CSS classes""" + + def test_language_swift_class(self): + """Test language-swift CSS class""" + detector = LanguageDetector() + classes = ['language-swift', 'highlight'] + assert detector.extract_language_from_classes(classes) == 'swift' + + def test_lang_swift_class(self): + """Test lang-swift CSS class""" + detector = LanguageDetector() + classes = ['lang-swift', 'code'] + assert detector.extract_language_from_classes(classes) == 'swift' + + def test_bare_swift_class(self): + """Test bare 'swift' class name""" + detector = LanguageDetector() + classes = ['swift', 'highlight'] + assert detector.extract_language_from_classes(classes) == 'swift' + + def test_detect_from_html_swift_class(self): + """Test HTML element with Swift CSS class""" + detector = LanguageDetector() + html = 'let x = 5' + soup = BeautifulSoup(html, 'html.parser') + elem = soup.find('code') + + lang, confidence = detector.detect_from_html(elem, 'let x = 5') + assert lang == 'swift' + assert confidence == 1.0 # CSS class = high confidence + + +class TestPureSwiftDetection: + """Test pure Swift syntax detection""" + + def test_func_with_return_type(self): + """Test Swift function with return type""" + detector = LanguageDetector() + code = """ + func calculateSum(a: Int, b: Int) -> Int { + return a + b + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.5 + + def test_struct_declaration(self): + """Test Swift struct declaration""" + detector = LanguageDetector() + code = """ + struct Person: Codable { + let name: String + var age: Int + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.6 + + def test_protocol_declaration(self): + """Test Swift protocol declaration""" + detector = LanguageDetector() + code = """ + protocol DataProvider { + associatedtype DataType + func fetchData() -> DataType + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.7 + + def test_extension_declaration(self): + """Test Swift extension""" + detector = LanguageDetector() + code = """ + extension String { + func trimmed() -> String { + return self.trimmingCharacters(in: .whitespaces) + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.7 + + def test_guard_let_unwrapping(self): + """Test Swift guard let optional unwrapping""" + detector = LanguageDetector() + code = """ + func process(value: String?) { + guard let unwrapped = value else { + return + } + print(unwrapped) + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.7 + + def test_if_let_unwrapping(self): + """Test Swift if let optional unwrapping""" + detector = LanguageDetector() + code = """ + if let name = optionalName { + print("Hello, \\(name)") + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.5 + + def test_closure_syntax(self): + """Test Swift closure syntax""" + detector = LanguageDetector() + code = """ + let numbers = [1, 2, 3, 4, 5] + let doubled = numbers.map { $0 * 2 } + let filtered = numbers.filter { (num) in num > 2 } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.5 + + def test_error_handling(self): + """Test Swift error handling (try/catch/throws)""" + detector = LanguageDetector() + code = """ + func loadData() throws -> Data { + guard let url = URL(string: "https://api.example.com") else { + throw NetworkError.invalidURL + } + return try Data(contentsOf: url) + } + + do { + let data = try loadData() + } catch { + print(error) + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.7 + + def test_async_await(self): + """Test Swift async/await (Swift 5.5+)""" + detector = LanguageDetector() + code = """ + func fetchUser() async throws -> User { + let url = URL(string: "https://api.example.com/user")! + let (data, _) = try await URLSession.shared.data(from: url) + return try JSONDecoder().decode(User.self, from: data) + } + + Task { + let user = try await fetchUser() + print(user.name) + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.7 + + def test_actor_declaration(self): + """Test Swift actor (Swift 5.5+)""" + detector = LanguageDetector() + code = """ + actor BankAccount { + private var balance: Double = 0 + + func deposit(_ amount: Double) { + balance += amount + } + + func getBalance() -> Double { + return balance + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.6 + + def test_generics_with_constraints(self): + """Test Swift generics with constraints""" + detector = LanguageDetector() + code = """ + func findItem(in array: [T], matching item: T) -> Int? { + for (index, element) in array.enumerated() where element == item { + return index + } + return nil + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.6 + + def test_enum_with_associated_values(self): + """Test Swift enum with associated values""" + detector = LanguageDetector() + code = """ + enum Result { + case success(Success) + case failure(Failure) + } + + enum NetworkError: Error { + case invalidURL + case noConnection + case timeout(seconds: Int) + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.4 # Enums without strong Swift keywords have lower confidence + + def test_opaque_types(self): + """Test Swift opaque types (some keyword)""" + detector = LanguageDetector() + code = """ + func makeShape() -> some Shape { + return Circle() + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.5 + + def test_property_observers(self): + """Test Swift property observers (willSet/didSet)""" + detector = LanguageDetector() + code = """ + class ViewModel { + var count: Int = 0 { + willSet { + print("Will set to \\(newValue)") + } + didSet { + print("Changed from \\(oldValue)") + } + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.5 + + def test_memory_management_weak(self): + """Test Swift memory management (weak var)""" + detector = LanguageDetector() + code = """ + class Parent { + weak var delegate: ParentDelegate? + + func setupChild() { + let child = Child() + child.parent = self + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.5 + + def test_memory_management_weak_self_in_closure(self): + """Test Swift weak self in closures""" + detector = LanguageDetector() + code = """ + class NetworkManager { + func fetchData() { + URLSession.shared.dataTask(with: url) { [weak self] data, response, error in + guard let self = self else { return } + self.handleResponse(data) + }.resume() + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.7 + + def test_memory_management_unowned(self): + """Test Swift unowned keyword""" + detector = LanguageDetector() + code = """ + class Customer { + unowned let creditCard: CreditCard + + init(card: CreditCard) { + self.creditCard = card + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.5 + + def test_string_interpolation(self): + """Test Swift string interpolation""" + detector = LanguageDetector() + code = """ + let name = "Alice" + let age = 30 + let message = "Hello, \\(name)! You are \\(age) years old." + print("Current value: \\(someVar)") + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.4 + + +class TestUIKitDetection: + """Test iOS/UIKit pattern detection""" + + def test_viewcontroller_lifecycle(self): + """Test UIViewController lifecycle methods""" + detector = LanguageDetector() + code = """ + import UIKit + + class HomeViewController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + def test_iboutlet_ibaction(self): + """Test @IBOutlet and @IBAction""" + detector = LanguageDetector() + code = """ + class LoginViewController: UIViewController { + @IBOutlet weak var usernameTextField: UITextField! + @IBOutlet weak var passwordTextField: UITextField! + @IBOutlet weak var loginButton: UIButton! + + @IBAction func loginTapped(_ sender: UIButton) { + authenticate() + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + def test_tableview_delegate(self): + """Test UITableView delegate/datasource""" + detector = LanguageDetector() + code = """ + extension ViewController: UITableViewDelegate, UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return items.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) + cell.textLabel?.text = items[indexPath.row] + return cell + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.8 + + def test_auto_layout_constraints(self): + """Test Auto Layout constraint code""" + detector = LanguageDetector() + code = """ + func setupConstraints() { + button.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + button.centerXAnchor.constraint(equalTo: view.centerXAnchor), + button.centerYAnchor.constraint(equalTo: view.centerYAnchor), + button.widthAnchor.constraint(equalToConstant: 200), + button.heightAnchor.constraint(equalToConstant: 50) + ]) + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.7 + + def test_dispatch_queue(self): + """Test DispatchQueue usage""" + detector = LanguageDetector() + code = """ + func fetchData() { + DispatchQueue.global(qos: .background).async { + let data = self.loadFromNetwork() + + DispatchQueue.main.async { [weak self] in + self?.updateUI(with: data) + } + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.8 + + def test_codable_json(self): + """Test Codable JSON encoding/decoding""" + detector = LanguageDetector() + code = """ + struct User: Codable { + let id: Int + let name: String + let email: String + + enum CodingKeys: String, CodingKey { + case id + case name + case email = "email_address" + } + } + + let decoder = JSONDecoder() + let user = try decoder.decode(User.self, from: jsonData) + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + +class TestAppKitDetection: + """Test macOS/AppKit pattern detection""" + + def test_nsviewcontroller_lifecycle(self): + """Test NSViewController lifecycle methods""" + detector = LanguageDetector() + code = """ + import AppKit + + class MainViewController: NSViewController { + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + } + + override func viewWillAppear() { + super.viewWillAppear() + } + + override func viewDidAppear() { + super.viewDidAppear() + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + def test_nswindow_controller(self): + """Test NSWindowController""" + detector = LanguageDetector() + code = """ + import Cocoa + + class PreferencesWindowController: NSWindowController { + override func windowDidLoad() { + super.windowDidLoad() + window?.title = "Preferences" + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.8 + + def test_nstableview_delegate(self): + """Test NSTableView delegate/datasource""" + detector = LanguageDetector() + code = """ + extension ViewController: NSTableViewDelegate, NSTableViewDataSource { + func numberOfRows(in tableView: NSTableView) -> Int { + return items.count + } + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("Cell"), owner: nil) as? NSTableCellView + cell?.textField?.stringValue = items[row] + return cell + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.8 + + def test_nsapplication_delegate(self): + """Test NSApplicationDelegate""" + detector = LanguageDetector() + code = """ + import Cocoa + + @main + class AppDelegate: NSObject, NSApplicationDelegate { + func applicationDidFinishLaunching(_ notification: Notification) { + // Setup code + } + + func applicationWillTerminate(_ notification: Notification) { + // Cleanup code + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + def test_nsmenu_toolbar(self): + """Test NSMenu and NSToolbar""" + detector = LanguageDetector() + code = """ + func setupMenu() { + let menu = NSMenu(title: "File") + let menuItem = NSMenuItem(title: "New", action: #selector(newDocument), keyEquivalent: "n") + menu.addItem(menuItem) + + let toolbar = NSToolbar(identifier: "MainToolbar") + toolbar.delegate = self + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.8 + + def test_nspanel_dialogs(self): + """Test NSOpenPanel and NSSavePanel""" + detector = LanguageDetector() + code = """ + func showOpenPanel() { + let panel = NSOpenPanel() + panel.allowsMultipleSelection = true + panel.canChooseDirectories = true + + if panel.runModal() == .OK { + for url in panel.urls { + processFile(at: url) + } + } + } + + func showSavePanel() { + let panel = NSSavePanel() + panel.allowedContentTypes = [.png] + + if panel.runModal() == .OK { + saveFile(to: panel.url!) + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.8 + + def test_nsstatusbar_menubar_extra(self): + """Test NSStatusBar for menu bar apps""" + detector = LanguageDetector() + code = """ + class StatusBarController { + private var statusItem: NSStatusItem? + + func setup() { + statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + statusItem?.button?.image = NSImage(systemSymbolName: "star", accessibilityDescription: nil) + + let menu = NSMenu() + menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) + statusItem?.menu = menu + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + +class TestSwiftUIDetection: + """Test SwiftUI pattern detection""" + + def test_basic_swiftui_view(self): + """Test basic SwiftUI View""" + detector = LanguageDetector() + code = """ + import SwiftUI + + struct ContentView: View { + var body: some View { + Text("Hello, World!") + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + def test_state_binding(self): + """Test @State and @Binding""" + detector = LanguageDetector() + code = """ + struct CounterView: View { + @State private var count = 0 + + var body: some View { + VStack { + Text("Count: \\(count)") + Button("Increment") { + count += 1 + } + } + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + def test_observable_object(self): + """Test @Published and @ObservedObject""" + detector = LanguageDetector() + code = """ + class UserViewModel: ObservableObject { + @Published var username = "" + @Published var isLoggedIn = false + } + + struct ProfileView: View { + @ObservedObject var viewModel: UserViewModel + + var body: some View { + Text(viewModel.username) + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + def test_environment_object(self): + """Test @EnvironmentObject and @Environment""" + detector = LanguageDetector() + code = """ + struct SettingsView: View { + @EnvironmentObject var settings: AppSettings + @Environment(\\.colorScheme) var colorScheme + + var body: some View { + Form { + Toggle("Dark Mode", isOn: $settings.darkModeEnabled) + } + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + def test_swiftui_stacks(self): + """Test VStack, HStack, ZStack""" + detector = LanguageDetector() + code = """ + struct LayoutView: View { + var body: some View { + VStack { + HStack { + Text("Left") + Spacer() + Text("Right") + } + ZStack { + Color.blue + Text("Overlay") + } + } + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + def test_swiftui_navigation(self): + """Test NavigationView/NavigationStack and NavigationLink""" + detector = LanguageDetector() + code = """ + struct MainView: View { + var body: some View { + NavigationStack { + List { + NavigationLink("Detail", destination: DetailView()) + } + .navigationTitle("Home") + } + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + def test_swiftui_modifiers(self): + """Test SwiftUI view modifiers""" + detector = LanguageDetector() + code = """ + Text("Hello") + .font(.title) + .foregroundColor(.blue) + .padding() + .background(Color.gray.opacity(0.2)) + .cornerRadius(10) + .shadow(radius: 5) + .onAppear { + print("View appeared") + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.8 + + def test_swiftui_list_foreach(self): + """Test List and ForEach""" + detector = LanguageDetector() + code = """ + struct ItemListView: View { + let items = ["Apple", "Banana", "Cherry"] + + var body: some View { + List { + ForEach(items, id: \\.self) { item in + Text(item) + } + } + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.8 + + def test_swiftui_sheet_alert(self): + """Test .sheet and .alert modifiers""" + detector = LanguageDetector() + code = """ + struct ContentView: View { + @State private var showSheet = false + @State private var showAlert = false + + var body: some View { + Button("Show Sheet") { showSheet = true } + .sheet(isPresented: $showSheet) { + Text("Sheet Content") + } + .alert("Alert Title", isPresented: $showAlert) { + Button("OK", role: .cancel) { } + } + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + def test_swiftui_macos_window_group(self): + """Test macOS SwiftUI WindowGroup and Scene""" + detector = LanguageDetector() + code = """ + import SwiftUI + + @main + struct MyMacApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + .windowStyle(.hiddenTitleBar) + + Settings { + SettingsView() + } + + MenuBarExtra("Status", systemImage: "star") { + MenuBarView() + } + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.95 + + def test_swiftui_navigation_split_view(self): + """Test NavigationSplitView (macOS/iPad)""" + detector = LanguageDetector() + code = """ + struct SidebarView: View { + @State private var selection: Item? + + var body: some View { + NavigationSplitView { + List(items, selection: $selection) { item in + NavigationLink(value: item) { + Text(item.title) + } + } + .navigationTitle("Sidebar") + } detail: { + if let selection { + DetailView(item: selection) + } else { + Text("Select an item") + } + } + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + def test_swift_observation(self): + """Test Swift 5.9 @Observable macro""" + detector = LanguageDetector() + code = """ + import SwiftUI + + @Observable + class ViewModel { + var items: [Item] = [] + var isLoading = false + } + + struct ContentView: View { + @Bindable var viewModel: ViewModel + + var body: some View { + List(viewModel.items) { item in + Text(item.name) + } + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + +class TestCombineDetection: + """Test Combine framework patterns""" + + def test_combine_publisher_subscriber(self): + """Test Combine Publisher and Subscriber""" + detector = LanguageDetector() + code = """ + import Combine + + class DataService { + private var cancellables = Set() + + func fetchData() -> AnyPublisher<[Item], Error> { + URLSession.shared.dataTaskPublisher(for: url) + .map(\\.data) + .decode(type: [Item].self, decoder: JSONDecoder()) + .receive(on: RunLoop.main) + .eraseToAnyPublisher() + } + + func subscribe() { + fetchData() + .sink { completion in + print(completion) + } receiveValue: { items in + self.items = items + } + .store(in: &cancellables) + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + def test_combine_subjects(self): + """Test PassthroughSubject and CurrentValueSubject""" + detector = LanguageDetector() + code = """ + import Combine + + class EventBus { + let messageSubject = PassthroughSubject() + let counterSubject = CurrentValueSubject(0) + + func sendMessage(_ message: String) { + messageSubject.send(message) + } + + func incrementCounter() { + counterSubject.value += 1 + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.9 + + +class TestSwiftConfidenceScoring: + """Test confidence scoring accuracy""" + + def test_minimal_swift_code(self): + """Test minimal Swift code (edge case)""" + detector = LanguageDetector() + # Note: "let x: Int = 5" is ambiguous with TypeScript + # Use guard let which is Swift-unique and gets high confidence + code = "guard let value = optional else { return }" + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.5 # guard let is very Swift-specific + + def test_high_confidence_full_app(self): + """Test complete SwiftUI app (high confidence expected)""" + detector = LanguageDetector() + code = """ + import SwiftUI + + @main + struct MyApp: App { + @StateObject private var viewModel = AppViewModel() + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(viewModel) + } + } + } + + struct ContentView: View { + @EnvironmentObject var viewModel: AppViewModel + @State private var searchText = "" + + var body: some View { + NavigationStack { + List { + ForEach(viewModel.filteredItems) { item in + NavigationLink(destination: DetailView(item: item)) { + ItemRow(item: item) + } + } + } + .navigationTitle("Items") + .searchable(text: $searchText) + .refreshable { + await viewModel.refresh() + } + } + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.95 + + def test_swift_vs_similar_languages(self): + """ + Test Swift doesn't false-positive for similar syntax in other languages. + + Critical for avoiding misclassification of: + - Go: 'func', ':=' short declaration + - Rust: 'fn', 'let mut', struct + - TypeScript: 'let', 'const', type annotations with ':' + + These languages share keywords or syntax patterns with Swift, + so detection must use unique Swift patterns (guard let, @State, etc.) + """ + detector = LanguageDetector() + + # Go code (also uses 'func') + go_code = """ + package main + + func main() { + message := "Hello" + fmt.Println(message) + } + """ + lang, _ = detector.detect_from_code(go_code) + assert lang == 'go', f"Expected 'go', got '{lang}'" + + # Rust code (also uses 'struct', 'fn') + rust_code = """ + fn main() { + let mut x = 5; + println!("Value: {}", x); + } + """ + lang, _ = detector.detect_from_code(rust_code) + assert lang == 'rust', f"Expected 'rust', got '{lang}'" + + # TypeScript code (similar type annotation syntax with ':') + ts_code = """ + interface User { + name: string; + age: number; + } + + const greet = (user: User): string => { + return `Hello, ${user.name}`; + } + + export type Status = 'active' | 'inactive'; + """ + lang, _ = detector.detect_from_code(ts_code) + assert lang == 'typescript', f"Expected 'typescript', got '{lang}'" + + +class TestSwiftEdgeCases: + """Test edge cases and error handling""" + + def test_swift_snippet_short(self): + """Test short Swift snippet""" + detector = LanguageDetector() + code = "guard let x = optional else { return }" + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.3 + + def test_swift_import_swiftui_only(self): + """Test SwiftUI import statement alone""" + detector = LanguageDetector() + code = "import SwiftUI" + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.4 + + def test_swift_import_uikit_only(self): + """Test UIKit import statement alone""" + detector = LanguageDetector() + code = "import UIKit" + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.4 + + def test_swift_import_appkit_only(self): + """Test AppKit import statement alone""" + detector = LanguageDetector() + code = "import AppKit" + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.4 + + def test_swift_with_comments(self): + """Test Swift code with comments""" + detector = LanguageDetector() + code = """ + /// A view that displays a greeting + struct GreetingView: View { + // The name to display + var name: String + + var body: some View { + Text("Hello, \\(name)!") + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.7 + + def test_swift_core_data(self): + """Test Core Data code""" + detector = LanguageDetector() + code = """ + import CoreData + + class DataController: ObservableObject { + let container: NSPersistentContainer + + init() { + container = NSPersistentContainer(name: "Model") + container.loadPersistentStores { description, error in + if let error = error { + fatalError("Core Data failed: \\(error)") + } + } + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.8 + + def test_swift_data(self): + """Test SwiftData code (iOS 17+)""" + detector = LanguageDetector() + code = """ + import SwiftData + + @Model + class Item { + var name: String + var timestamp: Date + + @Relationship(deleteRule: .cascade) + var children: [ChildItem] + + init(name: String) { + self.name = name + self.timestamp = Date() + } + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.8 + + +class TestFoundationModelsDetection: + """Test Foundation Models framework detection (iOS/macOS 26+)""" + + def test_foundation_models_import(self): + """Test FoundationModels import""" + detector = LanguageDetector() + code = "import FoundationModels" + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.4 + + def test_generable_macro(self): + """Test @Generable macro detection""" + detector = LanguageDetector() + code = """ + @Generable(description: "A movie recommendation") + struct MovieRecommendation { + @Guide(description: "The movie title") + var title: String + + @Guide(description: "Rating from 1-5", .range(1...5)) + var rating: Int + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.7 + + def test_language_model_session(self): + """Test LanguageModelSession usage""" + detector = LanguageDetector() + code = """ + import FoundationModels + + let session = LanguageModelSession(instructions: "You are a helpful assistant") + let response = try await session.respond(to: "Hello!") + print(response) + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.7 + + def test_system_language_model(self): + """Test SystemLanguageModel usage""" + detector = LanguageDetector() + code = """ + import FoundationModels + + let model = SystemLanguageModel.default + guard model.isAvailable else { + print("Model not available") + return + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.7 + + def test_streaming_response(self): + """Test streaming response pattern""" + detector = LanguageDetector() + code = """ + let session = LanguageModelSession(instructions: "Summarize text") + for try await partial in session.streamResponse(to: prompt) { + print(partial.content) + } + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.7 + + def test_guided_generation(self): + """Test guided generation with GeneratedContent""" + detector = LanguageDetector() + code = """ + let response = try await session.respond( + to: "Recommend a movie", + generating: MovieRecommendation.self + ) + print(response.title) + """ + lang, confidence = detector.detect_from_code(code) + assert lang == 'swift' + assert confidence >= 0.5 + + +class TestSwiftErrorHandling: + """Test error handling and graceful degradation for Swift detection""" + + def test_pattern_validation_catches_invalid_weight(self): + """Test that pattern validation catches invalid weight values""" + from skill_seekers.cli.swift_patterns import _validate_patterns + + # Invalid weight (too high) + invalid_patterns = { + 'test_lang': [ + (r'valid_pattern', 10), # Weight must be 1-5 + ] + } + + with pytest.raises(ValueError, match="weight must be int 1-5"): + _validate_patterns(invalid_patterns) + + def test_pattern_validation_catches_invalid_type(self): + """Test that pattern validation catches non-string patterns""" + from skill_seekers.cli.swift_patterns import _validate_patterns + + # Invalid pattern (not a string) + invalid_patterns = { + 'test_lang': [ + (12345, 5), # Pattern must be string + ] + } + + with pytest.raises(ValueError, match="regex must be a string"): + _validate_patterns(invalid_patterns) + + def test_pattern_validation_catches_invalid_tuple_structure(self): + """Test that pattern validation catches malformed tuples""" + from skill_seekers.cli.swift_patterns import _validate_patterns + + # Invalid structure (not a tuple) + invalid_patterns = { + 'test_lang': [ + "not_a_tuple", # Should be (pattern, weight) tuple + ] + } + + with pytest.raises(ValueError, match="is not a \\(regex, weight\\) tuple"): + _validate_patterns(invalid_patterns) + + def test_malformed_regex_patterns_are_skipped(self): + """Test that invalid regex patterns are logged and skipped without crashing""" + from skill_seekers.cli.language_detector import LanguageDetector + import logging + from unittest.mock import patch + + # Create detector - malformed patterns should be skipped during compilation + with patch('skill_seekers.cli.language_detector.logger') as mock_logger: + # Inject a language with a malformed pattern + import skill_seekers.cli.language_detector as ld_module + + # Save original patterns + original_patterns = ld_module.LANGUAGE_PATTERNS.copy() + + try: + # Add malformed pattern + ld_module.LANGUAGE_PATTERNS['test_malformed'] = [ + (r'(?P