From 91d869044697e3d1d7bd146dc6b0cba59c5e318c Mon Sep 17 00:00:00 2001 From: thejesh23 <35212698+thejesh23@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:58:32 -0700 Subject: [PATCH 1/4] feat(core): add Swift group to TEST_PATTERN_GROUPS with XCTest patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apple's XCTest and the newer Swift Testing framework (introduced with Xcode 16) both use the dominant `Tests.swift` naming convention — a single test file per unit-under-test. SPM already funnels those into a `Tests/` package directory (caught by the case-insensitive `tests` exact rule), but Xcode-authored codebases frequently keep them in `Tests/` folders (no leading dot, so the C#-style suffix rule misses them) and mixed SPM/Xcode repos sometimes colocate `*Tests.swift` next to `Sources/` files. Add: **/*Tests.swift — dominant Apple / Swift Testing convention **/*Test.swift — singular variant seen in some older codebases Kept minimal for the baseline commit; the BDD `*Spec.swift` glob (Quick/Nimble) follows separately for opt-in symmetry with the Ruby group's RSpec vs Minitest split. Tests: assert Swift sub-header + both globs; extend the stable-order invariant to place Swift after Ruby. --- .../core/src/__tests__/ignore-generator.test.ts | 13 ++++++++++++- .../packages/core/src/ignore-generator.ts | 13 +++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts b/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts index 5ee941ae..ad462bb0 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts @@ -284,12 +284,21 @@ describe("generateStarterIgnoreFile", () => { expect(content).toContain("# **/rails_helper.rb"); }); + it("includes Swift XCTest / Swift Testing file patterns", () => { + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# Swift"); + // Dominant Apple convention — one Tests.swift per unit. + expect(content).toContain("# **/*Tests.swift"); + // Singular variant seen in some older codebases. + expect(content).toContain("# **/*Test.swift"); + }); + it("groups patterns under the JS / TS sub-header", () => { const content = generateStarterIgnoreFile(testDir); expect(content).toContain("# JS / TS"); }); - it("emits language groups in stable order: JS, C#, Java, Go, C++, Python, Rust, Ruby", () => { + it("emits language groups in stable order: JS, C#, Java, Go, C++, Python, Rust, Ruby, Swift", () => { const content = generateStarterIgnoreFile(testDir); const jsIdx = content.indexOf("# JS / TS"); const csIdx = content.indexOf("# C# / .NET"); @@ -299,6 +308,7 @@ describe("generateStarterIgnoreFile", () => { const pyIdx = content.indexOf("# Python"); const rustIdx = content.indexOf("# Rust"); const rubyIdx = content.indexOf("# Ruby"); + const swiftIdx = content.indexOf("# Swift"); expect(jsIdx).toBeGreaterThan(-1); expect(csIdx).toBeGreaterThan(jsIdx); expect(javaIdx).toBeGreaterThan(csIdx); @@ -307,6 +317,7 @@ describe("generateStarterIgnoreFile", () => { expect(pyIdx).toBeGreaterThan(cppIdx); expect(rustIdx).toBeGreaterThan(pyIdx); expect(rubyIdx).toBeGreaterThan(rustIdx); + expect(swiftIdx).toBeGreaterThan(rubyIdx); }); it("keeps all suggestions commented even with no detected dirs and no .gitignore", () => { diff --git a/understand-anything-plugin/packages/core/src/ignore-generator.ts b/understand-anything-plugin/packages/core/src/ignore-generator.ts index 8d2769fc..412c7cc4 100644 --- a/understand-anything-plugin/packages/core/src/ignore-generator.ts +++ b/understand-anything-plugin/packages/core/src/ignore-generator.ts @@ -156,6 +156,19 @@ const TEST_PATTERN_GROUPS: Array<{ label: string; patterns: string[] }> = [ "**/rails_helper.rb", ], }, + { + // Apple's XCTest and the newer Swift Testing framework both use a + // dominant `Tests.swift` naming convention. SPM's `Tests/` + // package directory is already caught by the case-insensitive `tests` + // dir rule, but individual test files sometimes leak elsewhere + // (Xcode-style `Tests/` folders that use no leading dot, + // fixture-adjacent extension test files under Sources/). + label: "Swift", + patterns: [ + "**/*Tests.swift", + "**/*Test.swift", + ], + }, ]; /** From c5ec3b4d7a26437a3f3df3b601fe58fd9ef7469f Mon Sep 17 00:00:00 2001 From: thejesh23 <35212698+thejesh23@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:59:12 -0700 Subject: [PATCH 2/4] feat(core): add Quick/Nimble *Spec.swift to Swift group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quick is the Swift ecosystem's RSpec analogue — the dominant BDD-style testing framework before Apple shipped Swift Testing (Xcode 16, 2024). Files follow the `Spec.swift` convention and typically live under `Tests/` (SPM) or `Tests/` (Xcode) alongside XCTest files, so the dir rules do catch most of them — but small gem-style packages and CLI tools sometimes keep `*Spec.swift` at the `Sources/` root, which the dir rules miss. Kept as a separate opt-in glob from `*Tests.swift` so users on XCTest-only projects (Apple's official Swift repos, Alamofire) aren't carrying a comment for a framework they don't use. Mirrors the split in the Ruby group between RSpec `*_spec.rb` and Minitest `*_test.rb`. --- .../packages/core/src/__tests__/ignore-generator.test.ts | 7 +++++++ .../packages/core/src/ignore-generator.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts b/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts index ad462bb0..cc8664ba 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts @@ -293,6 +293,13 @@ describe("generateStarterIgnoreFile", () => { expect(content).toContain("# **/*Test.swift"); }); + it("includes Swift Quick/Nimble BDD *Spec.swift files", () => { + const content = generateStarterIgnoreFile(testDir); + // Quick is the Swift RSpec-equivalent — dominant in codebases that + // adopted BDD styling before Swift Testing shipped. + expect(content).toContain("# **/*Spec.swift"); + }); + it("groups patterns under the JS / TS sub-header", () => { const content = generateStarterIgnoreFile(testDir); expect(content).toContain("# JS / TS"); diff --git a/understand-anything-plugin/packages/core/src/ignore-generator.ts b/understand-anything-plugin/packages/core/src/ignore-generator.ts index 412c7cc4..3d27b4f2 100644 --- a/understand-anything-plugin/packages/core/src/ignore-generator.ts +++ b/understand-anything-plugin/packages/core/src/ignore-generator.ts @@ -167,6 +167,7 @@ const TEST_PATTERN_GROUPS: Array<{ label: string; patterns: string[] }> = [ patterns: [ "**/*Tests.swift", "**/*Test.swift", + "**/*Spec.swift", ], }, ]; From 3176a77e1767d94da9cbada6eeeb04acd466e1a5 Mon Sep 17 00:00:00 2001 From: thejesh23 <35212698+thejesh23@users.noreply.github.com> Date: Tue, 21 Jul 2026 03:01:34 -0700 Subject: [PATCH 3/4] docs(core): tighten Swift group rationale to reflect measured shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The initial comment speculated about Xcode `Tests/` folders being uncaught. Measurement across 10 major Swift repos showed the picture is the opposite of Ruby: the existing case-insensitive `tests` dir rule is already load-bearing, and the file globs earn only ~1% on the weighted total. Hero: signalapp/Signal-iOS at 1% (−0.07M tok, 31 inline-leaked *Tests.swift file hits under production modules like SignalServiceKit/Cryptography/). All 9 other repos measured at 0% because SPM funnels tests into `Tests/` (dir-caught) and even Xcode-authored consumer apps nest their unit tests under `test/`/`tests/`. Also record why no dir rules were added: - Xcode-style `*tests/` suffix (Scenario B) delivered ZERO additional bytes across the sample while carrying real false- positive risk on Contests/, Requests/, Interests/, Manifests/. - `uitests` exact rule (Scenario C) had zero hits — the codebases that use UI tests keep them under a nested `Tests/` folder that the existing rule already handles. Update the block comment so a future reader understands why Swift's file-glob group is minimal and why they shouldn't reach for dir rules to try to make it larger. --- .../packages/core/src/ignore-generator.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/understand-anything-plugin/packages/core/src/ignore-generator.ts b/understand-anything-plugin/packages/core/src/ignore-generator.ts index 3d27b4f2..3ac71119 100644 --- a/understand-anything-plugin/packages/core/src/ignore-generator.ts +++ b/understand-anything-plugin/packages/core/src/ignore-generator.ts @@ -157,12 +157,23 @@ const TEST_PATTERN_GROUPS: Array<{ label: string; patterns: string[] }> = [ ], }, { - // Apple's XCTest and the newer Swift Testing framework both use a - // dominant `Tests.swift` naming convention. SPM's `Tests/` - // package directory is already caught by the case-insensitive `tests` - // dir rule, but individual test files sometimes leak elsewhere - // (Xcode-style `Tests/` folders that use no leading dot, - // fixture-adjacent extension test files under Sources/). + // Swift is the opposite of Ruby: the existing case-insensitive + // `tests` exact-dir rule already sweeps up nearly every Swift test + // file — SPM funnels everything into `Tests/` by convention, and + // even Xcode consumer apps (Signal-iOS) keep their unit tests + // under nested `test/`/`tests/` dirs. Measurement across 10 major + // Swift repos (Apple stdlib, swift-nio, SPM, Alamofire, Vapor, + // realm-swift, TCA, Quick/Nimble, Signal-iOS, swift-snapshot- + // testing) put the file-glob contribution at ~1% weighted (max + // single-repo hero was Signal-iOS at 1% / −0.07M tok, 31 file + // hits). What the file globs still earn is the straggler case: + // test files scattered inline inside production modules + // (Signal-iOS's SignalServiceKit/Cryptography/CryptographyTests + // .swift, SPM's in-source *Tests.swift). No dir rules were added + // because the measurement showed both Xcode-style dir-suffix + // matching (`*tests/`) and a `uitests` exact rule delivered zero + // additional hits across the sample while `*tests/` carried real + // false-positive risk (Contests/, Requests/, Interests/). label: "Swift", patterns: [ "**/*Tests.swift", From cb4dfeff735b4458b7aa101d9bd4b8aefbadbdd1 Mon Sep 17 00:00:00 2001 From: thejesh23 <35212698+thejesh23@users.noreply.github.com> Date: Tue, 28 Jul 2026 22:21:36 -0700 Subject: [PATCH 4/4] fix(core): scope Swift ignore patterns to test dirs, not file suffixes The runtime matcher (`ignore` in ignore-filter.ts) runs with its default `ignorecase: true`, so the previously proposed file-suffix globs were far broader than they read: **/*Test.swift -> Contest.swift, Latest.swift, Backtest.swift, Protest.swift **/*Tests.swift -> Contests.swift **/*Spec.swift -> Inspec.swift This is not recoverable by writing the glob more carefully -- `ignore` compiles patterns to a RegExp with the `i` flag, so a [Tt] character class buys nothing. Since these starter lines exist to be uncommented wholesale, silently dropping production source is the worst available failure mode. Replaced with exact-name directory globs (**/Tests/**/*.swift, **/Specs/**/*.swift) which carry no false-positive risk at all and, unlike detectDirectories(), reach nested layouts such as Modules/Feature/Tests/. Cross-language change, called out for review: SUFFIX_DIR_GLOBS is broadened from [".tests", ".unittests", ".integrationtests"] to ["tests", "specs"]. The dotted C# forms all end in "tests", so three entries collapse to one with no behaviour lost. This is what recovers Xcode's MyAppTests/ and MyAppUITests/ targets. Suffix matching is safe *here* in a way the equivalent file glob is not: it only ever runs against directories that exist on disk, and emits them commented-out under their real name -- a repo with a genuine Contests/ dir sees a literal `# Contests/` line it can decline to uncomment. Note this now also applies outside Swift projects. Accepted gap: nested Xcode-style dirs (Modules/Feature/FeatureTests/) are reached by neither rule. Under-matching costs tokens; over-matching drops source. Only one of those is a correctness bug. Tests assert on real matcher behaviour rather than emitted text alone -- the gap that let the original patterns through review. The earlier token-savings figures are removed rather than restated: they were measured against the file-suffix shape and do not carry over. The directory-scoped shape still needs re-measuring. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/__tests__/ignore-generator.test.ts | 81 +++++++++++++++++-- .../packages/core/src/ignore-generator.ts | 77 +++++++++++------- 2 files changed, 124 insertions(+), 34 deletions(-) diff --git a/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts b/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts index cc8664ba..063325a9 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import ignore from "ignore"; import { generateStarterIgnoreFile } from "../ignore-generator"; import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; @@ -284,20 +285,86 @@ describe("generateStarterIgnoreFile", () => { expect(content).toContain("# **/rails_helper.rb"); }); - it("includes Swift XCTest / Swift Testing file patterns", () => { + it("scopes Swift XCTest patterns to exactly-named test directories", () => { const content = generateStarterIgnoreFile(testDir); expect(content).toContain("# Swift"); - // Dominant Apple convention — one Tests.swift per unit. - expect(content).toContain("# **/*Tests.swift"); - // Singular variant seen in some older codebases. - expect(content).toContain("# **/*Test.swift"); + // Exact name, not a *Tests suffix — Contests/ must not be matchable. + expect(content).toContain("# **/Tests/**/*.swift"); }); - it("includes Swift Quick/Nimble BDD *Spec.swift files", () => { + it("includes Swift Quick/Nimble BDD spec directories", () => { const content = generateStarterIgnoreFile(testDir); // Quick is the Swift RSpec-equivalent — dominant in codebases that // adopted BDD styling before Swift Testing shipped. - expect(content).toContain("# **/*Spec.swift"); + expect(content).toContain("# **/Specs/**/*.swift"); + }); + + it("suggests Xcode target dirs by real name, only when they exist", () => { + mkdirSync(join(testDir, "MyAppTests"), { recursive: true }); + mkdirSync(join(testDir, "MyAppUITests"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# MyAppTests/"); + expect(content).toContain("# MyAppUITests/"); + // Not speculatively globbed for projects that have no such dir. + expect(generateStarterIgnoreFile(join(testDir, "nope"))).not.toContain( + "MyAppTests/", + ); + }); + + it("surfaces a production *tests dir under its real name, not silently", () => { + // Contests/ does match the unanchored suffix rule — that is acceptable + // only because the user sees this exact line and can leave it commented. + mkdirSync(join(testDir, "Contests"), { recursive: true }); + const content = generateStarterIgnoreFile(testDir); + expect(content).toContain("# Contests/"); + }); + + it("keeps production Swift source when the Swift group is uncommented", () => { + // These starter lines exist to be uncommented, so assert on real + // matcher behaviour rather than on the emitted text alone. + const content = generateStarterIgnoreFile(testDir); + const swiftPatterns = content + .split("\n") + .map((l) => l.replace(/^#\s*/, "").trim()) + .filter((l) => l.endsWith(".swift")); + expect(swiftPatterns.length).toBeGreaterThan(0); + + const ig = ignore().add(swiftPatterns); + for (const kept of [ + "Sources/App/Contest.swift", + "Sources/App/Latest.swift", + "Sources/App/Backtest.swift", + "Sources/App/Protest.swift", + "Sources/App/Contests.swift", + "Sources/App/Inspec.swift", + "Sources/Requests/LoginRequest.swift", + "Sources/Interests/InterestPicker.swift", + // Directory-level collisions the exact-name globs must also avoid. + "Sources/Contests/ContestList.swift", + "Sources/Protests/ProtestFeed.swift", + ]) { + expect(ig.ignores(kept), `${kept} must not be ignored`).toBe(false); + } + for (const dropped of [ + "Tests/AppTests/AppTests.swift", + "Tests/AppTests/Helpers.swift", + "Tests/File.swift", + "Modules/Feature/Tests/A.swift", + "SignalServiceKit/tests/CryptoTest.swift", + "Specs/LoginSpec.swift", + ]) { + expect(ig.ignores(dropped), `${dropped} must be ignored`).toBe(true); + } + }); + + it("does not emit bare Swift file-suffix globs", () => { + const content = generateStarterIgnoreFile(testDir); + // The runtime matcher is case-insensitive (`ignore` defaults to + // ignorecase: true), so these would silently drop production files + // named Contest.swift / Latest.swift / Backtest.swift / Inspec.swift. + expect(content).not.toContain("# **/*Test.swift"); + expect(content).not.toContain("# **/*Tests.swift"); + expect(content).not.toContain("# **/*Spec.swift"); }); it("groups patterns under the JS / TS sub-header", () => { diff --git a/understand-anything-plugin/packages/core/src/ignore-generator.ts b/understand-anything-plugin/packages/core/src/ignore-generator.ts index 3ac71119..fca4605e 100644 --- a/understand-anything-plugin/packages/core/src/ignore-generator.ts +++ b/understand-anything-plugin/packages/core/src/ignore-generator.ts @@ -39,14 +39,22 @@ const EXACT_DIR_NAMES = [ ]; // Directory-name suffixes matched case-insensitively via String.endsWith. -// Primarily intended for C# / .NET project-suffix conventions like Foo.Tests, -// Foo.UnitTests, Foo.IntegrationTests, but note the match is unanchored — -// e.g. a hypothetical `.storybook.tests` would also match. Suggestions stay -// commented-out so the user reviews before activating. +// Covers C# / .NET project-suffix conventions (Foo.Tests, Foo.UnitTests, +// Foo.IntegrationTests) and Xcode target conventions (MyAppTests, +// MyAppUITests) in one rule — the dotted C# forms all end in "tests", so +// they need no separate entries. +// +// The match is deliberately unanchored, which means a production directory +// ending in "tests"/"specs" (Contests/, Protests/) also matches. That is +// safe *here* in a way the equivalent file-glob is not: this list only ever +// runs against directories that actually exist on disk (see +// detectDirectories), and the result is emitted commented-out under the +// directory's real name — so a repo with a genuine Contests/ dir sees a +// literal `# Contests/` line it can decline to uncomment. A speculative +// `**/*Tests/**` glob offers the user no such signal. const SUFFIX_DIR_GLOBS = [ - ".tests", - ".unittests", - ".integrationtests", + "tests", + "specs", ]; // Test file patterns grouped by language. Emitted as commented suggestions @@ -157,28 +165,43 @@ const TEST_PATTERN_GROUPS: Array<{ label: string; patterns: string[] }> = [ ], }, { - // Swift is the opposite of Ruby: the existing case-insensitive - // `tests` exact-dir rule already sweeps up nearly every Swift test - // file — SPM funnels everything into `Tests/` by convention, and - // even Xcode consumer apps (Signal-iOS) keep their unit tests - // under nested `test/`/`tests/` dirs. Measurement across 10 major - // Swift repos (Apple stdlib, swift-nio, SPM, Alamofire, Vapor, - // realm-swift, TCA, Quick/Nimble, Signal-iOS, swift-snapshot- - // testing) put the file-glob contribution at ~1% weighted (max - // single-repo hero was Signal-iOS at 1% / −0.07M tok, 31 file - // hits). What the file globs still earn is the straggler case: - // test files scattered inline inside production modules - // (Signal-iOS's SignalServiceKit/Cryptography/CryptographyTests - // .swift, SPM's in-source *Tests.swift). No dir rules were added - // because the measurement showed both Xcode-style dir-suffix - // matching (`*tests/`) and a `uitests` exact rule delivered zero - // additional hits across the sample while `*tests/` carried real - // false-positive risk (Contests/, Requests/, Interests/). + // Swift patterns are scoped to test *directories*, not file + // suffixes, because the runtime matcher (the `ignore` package in + // ignore-filter.ts) runs with its default `ignorecase: true`. Under + // case-insensitive matching a bare `**/*Test.swift` also swallows + // production names like Contest.swift, Latest.swift, Backtest.swift + // and Protest.swift; `**/*Tests.swift` catches Contests.swift, and + // `**/*Spec.swift` catches Inspec.swift. That is not recoverable by + // writing the glob more carefully — `ignore` compiles patterns to a + // RegExp with the `i` flag, so a `[Tt]` character class buys nothing. + // Since these starter lines are meant to be uncommented by users, + // silently dropping real source is the worst failure mode available, + // so the file-suffix form was abandoned. + // + // The globs below use *exact* directory names rather than a `*Tests` + // suffix, so they carry no false-positive risk at all: Contests/ and + // Protests/ do not match. They cover SPM's `Tests/` and Quick's + // `Specs/` at any depth, including nested module layouts like + // Modules/Feature/Tests/ that `detectDirectories()` cannot see (it + // enumerates only direct children of projectRoot). + // + // Xcode's target-suffix convention (`MyAppTests/`, `MyAppUITests/`) + // is deliberately NOT handled here. It is handled by the "tests" + // entry in SUFFIX_DIR_GLOBS instead, which only fires for directories + // observed on disk and emits them under their real name — see the + // note there for why that is the safe place for suffix matching. + // The residual gap is a *nested* Xcode-style dir (Modules/Feature/ + // FeatureTests/), which neither rule reaches. That is an accepted + // miss: under-matching costs tokens, over-matching silently drops + // source, and only one of those is a correctness bug. + // + // NOTE: token-savings measurement still to be redone for this + // directory-scoped shape — the earlier file-suffix figures do not + // carry over and have been removed rather than restated. label: "Swift", patterns: [ - "**/*Tests.swift", - "**/*Test.swift", - "**/*Spec.swift", + "**/Tests/**/*.swift", + "**/Specs/**/*.swift", ], }, ];