diff --git a/.github/workflows/validate-sdk-compliance.yml b/.github/workflows/validate-sdk-compliance.yml index 818e0a7..b6210f3 100644 --- a/.github/workflows/validate-sdk-compliance.yml +++ b/.github/workflows/validate-sdk-compliance.yml @@ -8,7 +8,7 @@ on: type: string default: sdk-compliance.yaml language: - description: SDK language for public API check — must match a parse- script (e.g. swift, javascript, dart) + description: SDK language for public API check — "swift", "javascript", or "dart" type: string required: true sdk-ref: @@ -45,7 +45,7 @@ jobs: check: name: Check public API against capability matrix - runs-on: ubuntu-latest + runs-on: ${{ inputs.language == 'swift' && 'macos-latest' || 'ubuntu-latest' }} steps: - name: Checkout PR branch uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -69,6 +69,17 @@ jobs: with: node-version: "22" + - name: Cache SPM dependencies + if: inputs.language == 'swift' + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: | + ~/.cache/org.swift.swiftpm + _sdk-pr/.build/repositories + _sdk-base/.build/repositories + key: spm-${{ runner.os }}-${{ hashFiles('_sdk-pr/Package.resolved') }} + restore-keys: spm-${{ runner.os }}- + - name: Install dependencies run: npm ci working-directory: _sdk-spec/scripts/capability-matrix @@ -85,21 +96,68 @@ jobs: run: dart pub get working-directory: _sdk-spec/scripts/dart_symbol_extractor + - name: Extract symbol graphs — PR + if: inputs.language == 'swift' + run: | + # || true: test targets may fail to extract on CI runners; library targets land first + swift package dump-symbol-graph \ + --minimum-access-level public \ + --skip-synthesized-members || true + SGDIR=$(find .build -maxdepth 3 -type d -name "symbolgraph") + NFILES=$(ls "$SGDIR"/*.symbols.json 2>/dev/null | wc -l | tr -d ' ') + if [ "$NFILES" -eq 0 ]; then echo "::error::No symbol graphs emitted"; exit 1; fi + jq -s '[.[] | .symbols[]]' "$SGDIR"/*.symbols.json \ + > "$GITHUB_WORKSPACE/pr-raw.json" + working-directory: _sdk-pr + + - name: Extract symbol graphs — base + if: inputs.language == 'swift' + run: | + swift package dump-symbol-graph \ + --minimum-access-level public \ + --skip-synthesized-members || true + SGDIR=$(find .build -maxdepth 3 -type d -name "symbolgraph") + NFILES=$(ls "$SGDIR"/*.symbols.json 2>/dev/null | wc -l | tr -d ' ') + if [ "$NFILES" -eq 0 ]; then echo "::error::No symbol graphs emitted"; exit 1; fi + jq -s '[.[] | .symbols[]]' "$SGDIR"/*.symbols.json \ + > "$GITHUB_WORKSPACE/base-raw.json" + working-directory: _sdk-base + + - name: Normalize symbol graphs — PR + if: inputs.language == 'swift' + run: | + npm run --silent normalize-symbolgraph -- \ + "$GITHUB_WORKSPACE/pr-raw.json" "$GITHUB_WORKSPACE/_sdk-pr" \ + > "$GITHUB_WORKSPACE/pr-symbols.json" + working-directory: _sdk-spec/scripts/capability-matrix + + - name: Normalize symbol graphs — base + if: inputs.language == 'swift' + run: | + npm run --silent normalize-symbolgraph -- \ + "$GITHUB_WORKSPACE/base-raw.json" "$GITHUB_WORKSPACE/_sdk-base" \ + > "$GITHUB_WORKSPACE/base-symbols.json" + working-directory: _sdk-spec/scripts/capability-matrix + - name: Resolve parse command - if: inputs.language != 'dart' + if: inputs.language != 'swift' && inputs.language != 'dart' id: resolve run: | case "${{ inputs.language }}" in - swift) echo "cmd=parse-swift" >> "$GITHUB_OUTPUT" ;; - javascript) echo "cmd=parse-ts" >> "$GITHUB_OUTPUT" ;; + javascript) echo "cmd=parse-ts" >> "$GITHUB_OUTPUT" ;; *) echo "::error::Unsupported language '${{ inputs.language }}'. Supported values: swift, javascript, dart"; exit 1 ;; esac - - name: Parse PR and base branch - if: inputs.language != 'dart' + - name: Parse PR branch + if: inputs.language != 'swift' && inputs.language != 'dart' run: | npm run --silent ${{ steps.resolve.outputs.cmd }} -- "$GITHUB_WORKSPACE/_sdk-pr" \ > "$GITHUB_WORKSPACE/pr-symbols.json" + working-directory: _sdk-spec/scripts/capability-matrix + + - name: Parse base branch + if: inputs.language != 'swift' && inputs.language != 'dart' + run: | npm run --silent ${{ steps.resolve.outputs.cmd }} -- "$GITHUB_WORKSPACE/_sdk-base" \ > "$GITHUB_WORKSPACE/base-symbols.json" working-directory: _sdk-spec/scripts/capability-matrix diff --git a/scripts/capability-matrix/package.json b/scripts/capability-matrix/package.json index 8859f87..84ae0c9 100644 --- a/scripts/capability-matrix/package.json +++ b/scripts/capability-matrix/package.json @@ -9,7 +9,7 @@ "validate:online": "tsx src/cli.ts validate --online", "validate-compliance": "tsx src/compliance-cli.ts", "parse-ts": "tsx src/parse-ts.ts", - "parse-swift": "tsx src/parse-swift.ts", + "normalize-symbolgraph": "tsx src/normalize-symbolgraph-cli.ts", "check-api-symbols": "tsx src/check-api-symbols.ts", "aggregate": "tsx src/aggregate.ts", "report": "tsx src/cli.ts report", diff --git a/scripts/capability-matrix/src/normalize-symbolgraph-cli.ts b/scripts/capability-matrix/src/normalize-symbolgraph-cli.ts new file mode 100644 index 0000000..26d7c6c --- /dev/null +++ b/scripts/capability-matrix/src/normalize-symbolgraph-cli.ts @@ -0,0 +1,18 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { normalizeSymbolGraph, type SymbolGraphSymbol } from "./normalize-symbolgraph.js"; + +async function main(): Promise { + const [,, rawPath, sdkRootArg] = process.argv; + if (!rawPath) { + console.error("Usage: normalize-symbolgraph [sdk-root]"); + process.exit(1); + } + + const sdkRoot = sdkRootArg ? resolve(sdkRootArg) : ""; + const symbols = JSON.parse(readFileSync(rawPath, "utf8")) as SymbolGraphSymbol[]; + const result = normalizeSymbolGraph(symbols, sdkRoot); + console.log(JSON.stringify(result, null, 2)); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/scripts/capability-matrix/src/normalize-symbolgraph.ts b/scripts/capability-matrix/src/normalize-symbolgraph.ts new file mode 100644 index 0000000..e1cd95b --- /dev/null +++ b/scripts/capability-matrix/src/normalize-symbolgraph.ts @@ -0,0 +1,68 @@ +import { relative } from "node:path"; +import type { ParsedSymbol, ParseResult } from "./ts-parser.js"; +export type { ParsedSymbol, ParseResult }; + +export interface SymbolGraphSymbol { + kind: { identifier: string }; + pathComponents: string[]; + location?: { uri: string }; +} + +// Kind identifiers that map to ParsedSymbol kinds. +// swift.deinit and all unrecognised kinds are skipped. +const KIND_MAP: Record = { + "swift.class": "class", + "swift.struct": "class", + "swift.enum": "class", + "swift.protocol": "class", + "swift.actor": "class", + "swift.func": "function", + "swift.func.op": "function", + "swift.method": "method", + "swift.type.method": "method", + "swift.init": "method", + "swift.subscript": "method", + "swift.type.subscript": "method", + "swift.property": "property", + "swift.type.property": "property", + "swift.enum.case": "property", + "swift.typealias": "variable", + "swift.associatedtype": "variable", + "swift.var": "variable", +}; + +export function normalizeSymbolGraph( + symbols: SymbolGraphSymbol[], + sdkRoot: string, +): ParseResult { + const result: ParsedSymbol[] = []; + + for (const sym of symbols) { + const kind = KIND_MAP[sym.kind.identifier]; + if (kind === undefined) continue; + + result.push({ + name: qualifiedName(sym.pathComponents), + kind, + file: resolveFile(sym.location?.uri, sdkRoot), + }); + } + + return { symbols: result }; +} + +function qualifiedName(pathComponents: string[]): string { + if (pathComponents.length === 0) return ""; + const parts = pathComponents.map((part, i) => { + if (i < pathComponents.length - 1) return part; + const parenIdx = part.indexOf("("); + return parenIdx >= 0 ? part.slice(0, parenIdx) : part; + }); + return parts.join("."); +} + +function resolveFile(uri: string | undefined, sdkRoot: string): string { + if (!uri) return ""; + const path = uri.startsWith("file://") ? uri.slice(7) : uri; + return sdkRoot ? relative(sdkRoot, path) : path; +} diff --git a/scripts/capability-matrix/src/parse-swift.ts b/scripts/capability-matrix/src/parse-swift.ts deleted file mode 100644 index fcd6e11..0000000 --- a/scripts/capability-matrix/src/parse-swift.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { parseSwiftProject } from "./swift-parser.js"; - -async function main(): Promise { - const projectPath = process.argv[2]; - if (!projectPath) { - console.error("Usage: parse-swift "); - process.exit(1); - } - - try { - const result = parseSwiftProject(projectPath); - console.log(JSON.stringify(result, null, 2)); - } catch (e) { - console.error(`Error: ${(e as Error).message}`); - process.exit(1); - } -} - -main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/scripts/capability-matrix/src/swift-parser.ts b/scripts/capability-matrix/src/swift-parser.ts deleted file mode 100644 index 3a193ef..0000000 --- a/scripts/capability-matrix/src/swift-parser.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { readFileSync, readdirSync, existsSync } from "node:fs"; -import { join, resolve, relative } from "node:path"; -import type { ParsedSymbol, ParseResult } from "./ts-parser.js"; -import { loadIgnore, type Ignore } from "./parse-ignore.js"; - -export type { ParsedSymbol, ParseResult }; - -// --------------------------------------------------------------------------- -// Internal helpers -// --------------------------------------------------------------------------- - -function findSwiftFiles(dir: string, root: string, ig: Ignore): string[] { - const results: string[] = []; - try { - for (const entry of readdirSync(dir, { withFileTypes: true })) { - if (entry.name.startsWith(".")) continue; - const full = join(dir, entry.name); - const rel = relative(root, full); - if (entry.isDirectory()) { - if (ig.ignores(rel + "/")) continue; - results.push(...findSwiftFiles(full, root, ig)); - } else if (entry.isFile() && entry.name.endsWith(".swift")) { - if (ig.ignores(rel)) continue; - results.push(full); - } - } - } catch { /* ignore unreadable dirs */ } - return results; -} - -// --------------------------------------------------------------------------- -// Single-file parser -// --------------------------------------------------------------------------- - -interface Context { - name: string; // possibly dotted: "AuthClient.Configuration" - depth: number; // brace depth after entering this context -} - -// Access modifiers that indicate a symbol is public API -const ACCESS_PATTERN = /\b(?:public|open)\b/; -// Declaration keywords for types -const TYPE_KW = /\b(class|struct|enum|actor|protocol)\b/; -// Declaration keyword for extensions -const EXT_KW = /\bextension\b/; - -export function extractFromSource(source: string, relPath: string): ParsedSymbol[] { - const symbols: ParsedSymbol[] = []; - const contextStack: Context[] = []; - let depth = 0; - - for (const rawLine of source.split("\n")) { - // Strip line comments (// and ///) – handles the most common case - const commentIdx = rawLine.indexOf("//"); - const line = commentIdx >= 0 ? rawLine.slice(0, commentIdx) : rawLine; - const trimmed = line.trim(); - - if (!trimmed) continue; - // Preprocessor directives don't affect declarations - if (trimmed.startsWith("#")) continue; - - const opens = countChar(line, "{"); - const closes = countChar(line, "}"); - const isPublic = ACCESS_PATTERN.test(trimmed); - const isTypeDecl = isPublic && TYPE_KW.test(trimmed); - const isExtDecl = EXT_KW.test(trimmed); - - const currentType = - contextStack.length > 0 ? contextStack[contextStack.length - 1].name : ""; - - // --- Emit symbols for declarations on this line --- - - if (isTypeDecl) { - const m = trimmed.match(/\b(class|struct|enum|actor|protocol)\b\s+(\w+)/); - if (m) { - const typeName = m[2]; - const qualifiedName = currentType ? `${currentType}.${typeName}` : typeName; - symbols.push({ name: qualifiedName, kind: "class", file: relPath }); - } - } else if (isPublic && currentType) { - // Inside a type context: emit public members - const funcM = trimmed.match(/\bfunc\b\s+(\w+)/); - const varM = !funcM && trimmed.match(/\b(?:var|let)\b\s+(\w+)/); - const typealiasM = !funcM && !varM && trimmed.match(/\btypealias\b\s+(\w+)/); - const isInit = !funcM && !varM && !typealiasM && /\binit\b/.test(trimmed); - - if (funcM) { - symbols.push({ name: `${currentType}.${funcM[1]}`, kind: "method", file: relPath }); - } else if (varM) { - symbols.push({ name: `${currentType}.${varM[1]}`, kind: "property", file: relPath }); - } else if (typealiasM) { - symbols.push({ name: `${currentType}.${typealiasM[1]}`, kind: "variable", file: relPath }); - } else if (isInit) { - symbols.push({ name: `${currentType}.init`, kind: "method", file: relPath }); - } - } else if (isPublic && !currentType) { - // Top-level public declarations - const funcM = trimmed.match(/\bfunc\b\s+(\w+)/); - const typealiasM = !funcM && trimmed.match(/\btypealias\b\s+(\w+)/); - if (funcM) { - symbols.push({ name: funcM[1], kind: "function", file: relPath }); - } else if (typealiasM) { - symbols.push({ name: typealiasM[1], kind: "variable", file: relPath }); - } - } - - // --- Update depth --- - depth += opens - closes; - - // Pop contexts that ended when closes brought depth below their enter depth - while ( - contextStack.length > 0 && - contextStack[contextStack.length - 1].depth > depth - ) { - contextStack.pop(); - } - - // Push new context if this line opens a type or extension body - if (opens > closes) { - if (isTypeDecl) { - const m = trimmed.match(/\b(class|struct|enum|actor|protocol)\b\s+(\w+)/); - if (m) { - const typeName = m[2]; - const qualifiedName = currentType ? `${currentType}.${typeName}` : typeName; - contextStack.push({ name: qualifiedName, depth }); - } - } else if (isExtDecl) { - // Extensions can extend dotted names: "extension AuthClient.Configuration" - const m = trimmed.match(/\bextension\b\s+([\w.]+)/); - if (m) contextStack.push({ name: m[1], depth }); - } - } - } - - return symbols; -} - -// --------------------------------------------------------------------------- -// Project-level entry point -// --------------------------------------------------------------------------- - -export function parseSwiftProject(projectRoot: string): ParseResult { - const root = resolve(projectRoot); - const ig = loadIgnore(root); - // SPM convention: Sources/ holds all public library targets - const srcDir = join(root, "Sources"); - const scanRoot = existsSync(srcDir) ? srcDir : root; - - const files = findSwiftFiles(scanRoot, root, ig); - const symbols: ParsedSymbol[] = []; - - for (const file of files) { - const source = readFileSync(file, "utf8"); - const relPath = relative(root, file); - symbols.push(...extractFromSource(source, relPath)); - } - - return { symbols }; -} - -// --------------------------------------------------------------------------- -// Utility -// --------------------------------------------------------------------------- - -function countChar(s: string, ch: string): number { - let count = 0; - for (let i = 0; i < s.length; i++) { - if (s[i] === ch) count++; - } - return count; -} diff --git a/scripts/capability-matrix/test/fixtures/symbolgraph-sample.json b/scripts/capability-matrix/test/fixtures/symbolgraph-sample.json new file mode 100644 index 0000000..15289be --- /dev/null +++ b/scripts/capability-matrix/test/fixtures/symbolgraph-sample.json @@ -0,0 +1,1116 @@ +[ + { + "kind": { + "identifier": "swift.class", + "displayName": "Class" + }, + "identifier": { + "precise": "s:9SampleLib11SimpleClassC", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "SimpleClass" + ], + "names": { + "title": "SimpleClass", + "navigator": [ + { + "kind": "identifier", + "spelling": "SimpleClass" + } + ], + "subHeading": [ + { + "kind": "keyword", + "spelling": "class" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "SimpleClass" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "class" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "SimpleClass" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 0, + "character": 13 + } + } + }, + { + "kind": { + "identifier": "swift.property", + "displayName": "Instance Property" + }, + "identifier": { + "precise": "s:9SampleLib11SimpleClassC5valueSSvp", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "SimpleClass", + "value" + ], + "names": { + "title": "value", + "subHeading": [ + { + "kind": "keyword", + "spelling": "var" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "value" + }, + { + "kind": "text", + "spelling": ": " + }, + { + "kind": "typeIdentifier", + "spelling": "String", + "preciseIdentifier": "s:SS" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "var" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "value" + }, + { + "kind": "text", + "spelling": ": " + }, + { + "kind": "typeIdentifier", + "spelling": "String", + "preciseIdentifier": "s:SS" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 1, + "character": 15 + } + } + }, + { + "kind": { + "identifier": "swift.init", + "displayName": "Initializer" + }, + "identifier": { + "precise": "s:9SampleLib11SimpleClassC5valueACSS_tcfc", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "SimpleClass", + "init(value:)" + ], + "names": { + "title": "init(value:)", + "subHeading": [ + { + "kind": "keyword", + "spelling": "init" + }, + { + "kind": "text", + "spelling": "(" + }, + { + "kind": "externalParam", + "spelling": "value" + }, + { + "kind": "text", + "spelling": ": " + }, + { + "kind": "typeIdentifier", + "spelling": "String", + "preciseIdentifier": "s:SS" + }, + { + "kind": "text", + "spelling": ")" + } + ] + }, + "functionSignature": { + "parameters": [ + { + "name": "value", + "declarationFragments": [ + { + "kind": "identifier", + "spelling": "value" + }, + { + "kind": "text", + "spelling": ": " + }, + { + "kind": "typeIdentifier", + "spelling": "String", + "preciseIdentifier": "s:SS" + } + ] + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "init" + }, + { + "kind": "text", + "spelling": "(" + }, + { + "kind": "externalParam", + "spelling": "value" + }, + { + "kind": "text", + "spelling": ": " + }, + { + "kind": "typeIdentifier", + "spelling": "String", + "preciseIdentifier": "s:SS" + }, + { + "kind": "text", + "spelling": ")" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 2, + "character": 11 + } + } + }, + { + "kind": { + "identifier": "swift.method", + "displayName": "Instance Method" + }, + "identifier": { + "precise": "s:9SampleLib11SimpleClassC14instanceMethodSSyF", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "SimpleClass", + "instanceMethod()" + ], + "names": { + "title": "instanceMethod()", + "subHeading": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "instanceMethod" + }, + { + "kind": "text", + "spelling": "() -> " + }, + { + "kind": "typeIdentifier", + "spelling": "String", + "preciseIdentifier": "s:SS" + } + ] + }, + "functionSignature": { + "returns": [ + { + "kind": "typeIdentifier", + "spelling": "String", + "preciseIdentifier": "s:SS" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "instanceMethod" + }, + { + "kind": "text", + "spelling": "() -> " + }, + { + "kind": "typeIdentifier", + "spelling": "String", + "preciseIdentifier": "s:SS" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 3, + "character": 16 + } + } + }, + { + "kind": { + "identifier": "swift.type.method", + "displayName": "Type Method" + }, + "identifier": { + "precise": "s:9SampleLib11SimpleClassC12staticMethodACyFZ", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "SimpleClass", + "staticMethod()" + ], + "names": { + "title": "staticMethod()", + "subHeading": [ + { + "kind": "keyword", + "spelling": "static" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "staticMethod" + }, + { + "kind": "text", + "spelling": "() -> " + }, + { + "kind": "typeIdentifier", + "spelling": "SimpleClass", + "preciseIdentifier": "s:9SampleLib11SimpleClassC" + } + ] + }, + "functionSignature": { + "returns": [ + { + "kind": "typeIdentifier", + "spelling": "SimpleClass", + "preciseIdentifier": "s:9SampleLib11SimpleClassC" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "static" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "staticMethod" + }, + { + "kind": "text", + "spelling": "() -> " + }, + { + "kind": "typeIdentifier", + "spelling": "SimpleClass", + "preciseIdentifier": "s:9SampleLib11SimpleClassC" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 4, + "character": 23 + } + } + }, + { + "kind": { + "identifier": "swift.struct", + "displayName": "Structure" + }, + "identifier": { + "precise": "s:9SampleLib11SimpleClassC12NestedStructV", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "SimpleClass", + "NestedStruct" + ], + "names": { + "title": "SimpleClass.NestedStruct", + "navigator": [ + { + "kind": "identifier", + "spelling": "NestedStruct" + } + ], + "subHeading": [ + { + "kind": "keyword", + "spelling": "struct" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "NestedStruct" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "struct" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "NestedStruct" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 5, + "character": 18 + } + } + }, + { + "kind": { + "identifier": "swift.init", + "displayName": "Initializer" + }, + "identifier": { + "precise": "s:9SampleLib11SimpleClassC12NestedStructVAEycfc", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "SimpleClass", + "NestedStruct", + "init()" + ], + "names": { + "title": "init()", + "subHeading": [ + { + "kind": "keyword", + "spelling": "init" + }, + { + "kind": "text", + "spelling": "()" + } + ] + }, + "functionSignature": {}, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "init" + }, + { + "kind": "text", + "spelling": "()" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 5, + "character": 40 + } + } + }, + { + "kind": { + "identifier": "swift.class", + "displayName": "Class" + }, + "identifier": { + "precise": "s:9SampleLib9OpenClassC", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "OpenClass" + ], + "names": { + "title": "OpenClass", + "navigator": [ + { + "kind": "identifier", + "spelling": "OpenClass" + } + ], + "subHeading": [ + { + "kind": "keyword", + "spelling": "class" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "OpenClass" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "class" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "OpenClass" + } + ], + "accessLevel": "open", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 7, + "character": 11 + } + } + }, + { + "kind": { + "identifier": "swift.init", + "displayName": "Initializer" + }, + "identifier": { + "precise": "s:9SampleLib9OpenClassCACycfc", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "OpenClass", + "init()" + ], + "names": { + "title": "init()", + "subHeading": [ + { + "kind": "keyword", + "spelling": "init" + }, + { + "kind": "text", + "spelling": "()" + } + ] + }, + "functionSignature": {}, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "init" + }, + { + "kind": "text", + "spelling": "()" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 8, + "character": 11 + } + } + }, + { + "kind": { + "identifier": "swift.method", + "displayName": "Instance Method" + }, + "identifier": { + "precise": "s:9SampleLib9OpenClassC10openMethodyyF", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "OpenClass", + "openMethod()" + ], + "names": { + "title": "openMethod()", + "subHeading": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "openMethod" + }, + { + "kind": "text", + "spelling": "()" + } + ] + }, + "functionSignature": { + "returns": [ + { + "kind": "text", + "spelling": "()" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "openMethod" + }, + { + "kind": "text", + "spelling": "()" + } + ], + "accessLevel": "open", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 9, + "character": 14 + } + } + }, + { + "kind": { + "identifier": "swift.enum", + "displayName": "Enumeration" + }, + "identifier": { + "precise": "s:9SampleLib10SimpleEnumO", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "SimpleEnum" + ], + "names": { + "title": "SimpleEnum", + "navigator": [ + { + "kind": "identifier", + "spelling": "SimpleEnum" + } + ], + "subHeading": [ + { + "kind": "keyword", + "spelling": "enum" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "SimpleEnum" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "enum" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "SimpleEnum" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 12, + "character": 12 + } + } + }, + { + "kind": { + "identifier": "swift.enum.case", + "displayName": "Case" + }, + "identifier": { + "precise": "s:9SampleLib10SimpleEnumO5alphayA2CmF", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "SimpleEnum", + "alpha" + ], + "names": { + "title": "SimpleEnum.alpha", + "subHeading": [ + { + "kind": "keyword", + "spelling": "case" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "alpha" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "case" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "alpha" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 12, + "character": 30 + } + } + }, + { + "kind": { + "identifier": "swift.enum.case", + "displayName": "Case" + }, + "identifier": { + "precise": "s:9SampleLib10SimpleEnumO4betayA2CmF", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "SimpleEnum", + "beta" + ], + "names": { + "title": "SimpleEnum.beta", + "subHeading": [ + { + "kind": "keyword", + "spelling": "case" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "beta" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "case" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "beta" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 12, + "character": 37 + } + } + }, + { + "kind": { + "identifier": "swift.protocol", + "displayName": "Protocol" + }, + "identifier": { + "precise": "s:9SampleLib14SimpleProtocolP", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "SimpleProtocol" + ], + "names": { + "title": "SimpleProtocol", + "navigator": [ + { + "kind": "identifier", + "spelling": "SimpleProtocol" + } + ], + "subHeading": [ + { + "kind": "keyword", + "spelling": "protocol" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "SimpleProtocol" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "protocol" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "SimpleProtocol" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 13, + "character": 16 + } + } + }, + { + "kind": { + "identifier": "swift.method", + "displayName": "Instance Method" + }, + "identifier": { + "precise": "s:9SampleLib14SimpleProtocolP11requirementyyF", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "SimpleProtocol", + "requirement()" + ], + "names": { + "title": "requirement()", + "subHeading": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "requirement" + }, + { + "kind": "text", + "spelling": "()" + } + ] + }, + "functionSignature": { + "returns": [ + { + "kind": "text", + "spelling": "()" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "requirement" + }, + { + "kind": "text", + "spelling": "()" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 13, + "character": 38 + } + } + }, + { + "kind": { + "identifier": "swift.func", + "displayName": "Function" + }, + "identifier": { + "precise": "s:9SampleLib14globalFunctionyyF", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "globalFunction()" + ], + "names": { + "title": "globalFunction()", + "subHeading": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "globalFunction" + }, + { + "kind": "text", + "spelling": "()" + } + ] + }, + "functionSignature": { + "returns": [ + { + "kind": "text", + "spelling": "()" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "globalFunction" + }, + { + "kind": "text", + "spelling": "()" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 14, + "character": 12 + } + } + }, + { + "kind": { + "identifier": "swift.var", + "displayName": "Global Variable" + }, + "identifier": { + "precise": "s:9SampleLib9globalVarSivp", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "globalVar" + ], + "names": { + "title": "globalVar", + "subHeading": [ + { + "kind": "keyword", + "spelling": "var" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "globalVar" + }, + { + "kind": "text", + "spelling": ": " + }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "var" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "globalVar" + }, + { + "kind": "text", + "spelling": ": " + }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///sdk-root/Sources/SampleLib/SampleLib.swift", + "position": { + "line": 15, + "character": 11 + } + } + } +] diff --git a/scripts/capability-matrix/test/normalize-symbolgraph.test.ts b/scripts/capability-matrix/test/normalize-symbolgraph.test.ts new file mode 100644 index 0000000..78d4c5c --- /dev/null +++ b/scripts/capability-matrix/test/normalize-symbolgraph.test.ts @@ -0,0 +1,215 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { normalizeSymbolGraph, type SymbolGraphSymbol } from "../src/normalize-symbolgraph.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// Helper to build a minimal SymbolGraphSymbol for inline tests. +function sym( + identifier: string, + pathComponents: string[], + uri?: string, +): SymbolGraphSymbol { + return { + kind: { identifier }, + pathComponents, + ...(uri ? { location: { uri } } : {}), + }; +} + +// --------------------------------------------------------------------------- +// Kind mapping +// --------------------------------------------------------------------------- + +describe("kind mapping — types", () => { + it("maps swift.class to 'class'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.class", ["MyClass"])], ""); + expect(symbols[0]).toMatchObject({ name: "MyClass", kind: "class" }); + }); + it("maps swift.struct to 'class'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.struct", ["MyStruct"])], ""); + expect(symbols[0]).toMatchObject({ name: "MyStruct", kind: "class" }); + }); + it("maps swift.enum to 'class'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.enum", ["MyEnum"])], ""); + expect(symbols[0]).toMatchObject({ name: "MyEnum", kind: "class" }); + }); + it("maps swift.protocol to 'class'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.protocol", ["MyProto"])], ""); + expect(symbols[0]).toMatchObject({ name: "MyProto", kind: "class" }); + }); + it("maps swift.actor to 'class'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.actor", ["MyActor"])], ""); + expect(symbols[0]).toMatchObject({ name: "MyActor", kind: "class" }); + }); +}); + +describe("kind mapping — callables", () => { + it("maps swift.func (top-level) to 'function'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.func", ["globalFn()"])], ""); + expect(symbols[0]).toMatchObject({ name: "globalFn", kind: "function" }); + }); + it("maps swift.func.op to 'function'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.func.op", ["==(_:_:)"])], ""); + expect(symbols[0]).toMatchObject({ name: "==", kind: "function" }); + }); + it("maps swift.method to 'method'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.method", ["MyClass", "doThing()"])], ""); + expect(symbols[0]).toMatchObject({ name: "MyClass.doThing", kind: "method" }); + }); + it("maps swift.type.method (static) to 'method'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.type.method", ["MyClass", "create()"])], ""); + expect(symbols[0]).toMatchObject({ name: "MyClass.create", kind: "method" }); + }); + it("maps swift.init to 'method' and strips signature", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.init", ["MyClass", "init(url:key:)"])], ""); + expect(symbols[0]).toMatchObject({ name: "MyClass.init", kind: "method" }); + }); + it("maps swift.subscript to 'method'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.subscript", ["MyClass", "subscript(_:)"])], ""); + expect(symbols[0]).toMatchObject({ name: "MyClass.subscript", kind: "method" }); + }); +}); + +describe("kind mapping — properties and variables", () => { + it("maps swift.property to 'property'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.property", ["MyClass", "value"])], ""); + expect(symbols[0]).toMatchObject({ name: "MyClass.value", kind: "property" }); + }); + it("maps swift.type.property (static) to 'property'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.type.property", ["MyClass", "shared"])], ""); + expect(symbols[0]).toMatchObject({ name: "MyClass.shared", kind: "property" }); + }); + it("maps swift.enum.case to 'property'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.enum.case", ["MyEnum", "alpha"])], ""); + expect(symbols[0]).toMatchObject({ name: "MyEnum.alpha", kind: "property" }); + }); + it("maps swift.typealias to 'variable'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.typealias", ["MyClass", "Callback"])], ""); + expect(symbols[0]).toMatchObject({ name: "MyClass.Callback", kind: "variable" }); + }); + it("maps swift.associatedtype to 'variable'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.associatedtype", ["MyProto", "Item"])], ""); + expect(symbols[0]).toMatchObject({ name: "MyProto.Item", kind: "variable" }); + }); + it("maps swift.var (global) to 'variable'", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.var", ["globalVar"])], ""); + expect(symbols[0]).toMatchObject({ name: "globalVar", kind: "variable" }); + }); +}); + +// --------------------------------------------------------------------------- +// Skipped kinds +// --------------------------------------------------------------------------- + +describe("skipped kinds", () => { + it("skips swift.deinit", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.deinit", ["MyClass", "deinit"])], ""); + expect(symbols).toHaveLength(0); + }); + it("skips unrecognised kind identifiers", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.unknown.thing", ["Something"])], ""); + expect(symbols).toHaveLength(0); + }); +}); + +// --------------------------------------------------------------------------- +// Name construction +// --------------------------------------------------------------------------- + +describe("name construction", () => { + it("joins pathComponents with '.'", () => { + const { symbols } = normalizeSymbolGraph( + [sym("swift.method", ["SupabaseClient", "signIn(email:password:)"])], "" + ); + expect(symbols[0].name).toBe("SupabaseClient.signIn"); + }); + it("handles deeply nested types", () => { + const { symbols } = normalizeSymbolGraph( + [sym("swift.property", ["Outer", "Inner", "value"])], "" + ); + expect(symbols[0].name).toBe("Outer.Inner.value"); + }); + it("strips trailing function signature from last pathComponent", () => { + const { symbols } = normalizeSymbolGraph( + [sym("swift.method", ["Auth", "signUp(email:password:captchaToken:)"])], "" + ); + expect(symbols[0].name).toBe("Auth.signUp"); + }); + it("leaves non-function pathComponents unchanged", () => { + const { symbols } = normalizeSymbolGraph( + [sym("swift.property", ["Auth", "session"])], "" + ); + expect(symbols[0].name).toBe("Auth.session"); + }); +}); + +// --------------------------------------------------------------------------- +// File path resolution +// --------------------------------------------------------------------------- + +describe("file path resolution", () => { + it("strips 'file://' prefix and makes path relative to sdkRoot", () => { + const sdkRoot = "/home/runner/work/supabase-swift"; + const uri = `file://${sdkRoot}/Sources/Auth/AuthClient.swift`; + const { symbols } = normalizeSymbolGraph([sym("swift.class", ["Auth"], uri)], sdkRoot); + expect(symbols[0].file).toBe("Sources/Auth/AuthClient.swift"); + }); + it("returns empty string when location is absent", () => { + const { symbols } = normalizeSymbolGraph([sym("swift.class", ["Auth"])], "/any/root"); + expect(symbols[0].file).toBe(""); + }); +}); + +// --------------------------------------------------------------------------- +// Multi-input +// --------------------------------------------------------------------------- + +describe("multi-symbol input", () => { + it("handles symbols from multiple modules in merged flat array", () => { + const input: SymbolGraphSymbol[] = [ + sym("swift.class", ["ClassFromAuth"]), + sym("swift.struct", ["StructFromStorage"]), + ]; + const { symbols } = normalizeSymbolGraph(input, ""); + const names = symbols.map(s => s.name); + expect(names).toContain("ClassFromAuth"); + expect(names).toContain("StructFromStorage"); + }); +}); + +// --------------------------------------------------------------------------- +// Smoke test against real fixture +// --------------------------------------------------------------------------- + +describe("real fixture smoke test", () => { + const fixture = JSON.parse( + readFileSync(join(__dirname, "fixtures/symbolgraph-sample.json"), "utf8") + ) as SymbolGraphSymbol[]; + + const { symbols } = normalizeSymbolGraph(fixture, "/sdk-root"); + + it("produces a non-empty symbol list", () => { + expect(symbols.length).toBeGreaterThan(10); + }); + it("includes SimpleClass from fixture", () => { + expect(symbols.map(s => s.name)).toContain("SimpleClass"); + }); + it("includes SimpleClass.instanceMethod from fixture", () => { + expect(symbols.map(s => s.name)).toContain("SimpleClass.instanceMethod"); + }); + it("includes SimpleEnum.alpha (enum case) from fixture", () => { + expect(symbols.map(s => s.name)).toContain("SimpleEnum.alpha"); + }); + it("includes OpenClass from fixture (open access level)", () => { + expect(symbols.map(s => s.name)).toContain("OpenClass"); + }); + it("includes globalFunction from fixture", () => { + expect(symbols.map(s => s.name)).toContain("globalFunction"); + }); + it("includes SimpleClass.NestedStruct (nested type) from fixture", () => { + expect(symbols.map(s => s.name)).toContain("SimpleClass.NestedStruct"); + }); +}); diff --git a/scripts/capability-matrix/test/swift-parser.test.ts b/scripts/capability-matrix/test/swift-parser.test.ts deleted file mode 100644 index 666449f..0000000 --- a/scripts/capability-matrix/test/swift-parser.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { join } from "node:path"; -import { tmpdir } from "node:os"; -import { writeFileSync, mkdirSync } from "node:fs"; -import { extractFromSource, parseSwiftProject } from "../src/swift-parser"; - -function names(src: string): string[] { - return extractFromSource(src, "src/Foo.swift").map((s) => s.name); -} - -describe("extractFromSource — type declarations", () => { - it("extracts a public class", () => { - expect(names("public class AuthClient {\n}")).toContain("AuthClient"); - }); - - it("extracts a public final class", () => { - expect(names("public final class SupabaseClient {\n}")).toContain("SupabaseClient"); - }); - - it("extracts a public struct", () => { - expect(names("public struct Session {\n}")).toContain("Session"); - }); - - it("extracts a public actor", () => { - expect(names("public actor AuthClient {\n}")).toContain("AuthClient"); - }); - - it("extracts a public protocol", () => { - expect(names("public protocol AuthStateChangeListenerRegistration {\n}")).toContain( - "AuthStateChangeListenerRegistration", - ); - }); - - it("does not extract a non-public class", () => { - expect(names("class Internal {\n public func foo() {}\n}")).not.toContain("Internal"); - }); -}); - -describe("extractFromSource — public class members", () => { - const src = ` -public class AuthClient { - public var session: Session { get async throws {} } - public func signUp(email: String) async throws -> Session {} - private var _token: String? - internal func _internal() {} - nonisolated public var currentSession: Session? - nonisolated public func handle(_ url: URL) {} - public init(configuration: Configuration) {} - public typealias EventCallback = @Sendable (AuthChangeEvent) -> Void -} -`; - it("includes public var", () => expect(names(src)).toContain("AuthClient.session")); - it("includes public func", () => expect(names(src)).toContain("AuthClient.signUp")); - it("includes nonisolated public var", () => expect(names(src)).toContain("AuthClient.currentSession")); - it("includes nonisolated public func", () => expect(names(src)).toContain("AuthClient.handle")); - it("includes public init", () => expect(names(src)).toContain("AuthClient.init")); - it("includes public typealias", () => expect(names(src)).toContain("AuthClient.EventCallback")); - it("excludes private var", () => expect(names(src)).not.toContain("AuthClient._token")); - it("excludes internal func", () => expect(names(src)).not.toContain("AuthClient._internal")); -}); - -describe("extractFromSource — static members", () => { - const src = ` -public class FunctionsClient { - public static let defaultTimeout: TimeInterval = 150 - public static func create() -> FunctionsClient {} -} -`; - it("includes public static let", () => expect(names(src)).toContain("FunctionsClient.defaultTimeout")); - it("includes public static func", () => expect(names(src)).toContain("FunctionsClient.create")); -}); - -describe("extractFromSource — plain extensions", () => { - const src = ` -extension AuthClient { - public func signOut() async throws {} - func internalHelper() {} -} -`; - it("includes explicitly public members in extension", () => - expect(names(src)).toContain("AuthClient.signOut")); - it("excludes non-public members in extension", () => - expect(names(src)).not.toContain("AuthClient.internalHelper")); -}); - -describe("extractFromSource — dotted extension names", () => { - const src = ` -extension AuthClient.Configuration { - public static let defaultLocalStorage: any AuthLocalStorage = KeychainLocalStorage() -} -`; - it("handles dotted extension name", () => - expect(names(src)).toContain("AuthClient.Configuration.defaultLocalStorage")); -}); - -describe("extractFromSource — nested types", () => { - const src = ` -public class SupabaseClient { - public struct Configuration { - public var url: URL - } - public func from(_ table: String) -> PostgrestQueryBuilder {} -} -`; - it("extracts nested struct", () => expect(names(src)).toContain("SupabaseClient.Configuration")); - it("extracts nested struct property", () => expect(names(src)).toContain("SupabaseClient.Configuration.url")); - it("extracts outer class method", () => expect(names(src)).toContain("SupabaseClient.from")); -}); - -describe("extractFromSource — multiline function signatures", () => { - const src = ` -public class AuthClient { - public func signUp( - email: String, - password: String, - captchaToken: String? = nil - ) async throws -> Session { - return Session() - } -} -`; - it("captures method whose signature spans multiple lines", () => - expect(names(src)).toContain("AuthClient.signUp")); -}); - -describe("extractFromSource — comment stripping", () => { - it("ignores symbols in line comments", () => { - const src = ` -public class Foo { - // public func notReal() {} - public func real() {} -} -`; - const n = names(src); - expect(n).toContain("Foo.real"); - expect(n).not.toContain("Foo.notReal"); - }); - - it("ignores doc comment lines", () => { - const src = ` -public class Foo { - /// public func docComment() {} - public func real() {} -} -`; - const n = names(src); - expect(n).toContain("Foo.real"); - expect(n).not.toContain("Foo.docComment"); - }); -}); - -describe("extractFromSource — context stack correctness", () => { - it("does not bleed context across sibling types", () => { - const src = ` -public class Auth { - public func signUp() {} -} -public class Storage { - public func upload() {} -} -`; - const n = names(src); - expect(n).toContain("Auth.signUp"); - expect(n).toContain("Storage.upload"); - expect(n).not.toContain("Auth.upload"); - expect(n).not.toContain("Storage.signUp"); - }); -}); - -describe("parseSwiftProject — .sdk-parse-ignore", () => { - it("excludes files matched by .sdk-parse-ignore", () => { - const dir = join(tmpdir(), `swift-parser-ignore-test-${process.pid}`); - const srcDir = join(dir, "Sources", "MyLib"); - mkdirSync(srcDir, { recursive: true }); - writeFileSync(join(srcDir, "Auth.swift"), "public class AuthClient {\n public func signUp() {}\n}\n"); - writeFileSync(join(dir, ".sdk-parse-ignore"), "Sources/MyLib/Auth.swift\n"); - const result = parseSwiftProject(dir); - expect(result.symbols.map((s) => s.name)).not.toContain("AuthClient"); - }); - - it("excludes entire directories matched by .sdk-parse-ignore", () => { - const dir = join(tmpdir(), `swift-parser-dir-ignore-test-${process.pid}`); - const testDir = join(dir, "Tests"); - mkdirSync(testDir, { recursive: true }); - writeFileSync(join(dir, "Client.swift"), "public class SupabaseClient {}\n"); - writeFileSync(join(testDir, "ClientTests.swift"), "public class SupabaseClientTests {}\n"); - writeFileSync(join(dir, ".sdk-parse-ignore"), "Tests/\n"); - const result = parseSwiftProject(dir); - const names = result.symbols.map((s) => s.name); - expect(names).toContain("SupabaseClient"); - expect(names).not.toContain("SupabaseClientTests"); - }); - - it("does not filter when .sdk-parse-ignore is absent", () => { - const dir = join(tmpdir(), `swift-parser-no-ignore-test-${process.pid}`); - const srcDir = join(dir, "Sources", "MyLib"); - mkdirSync(srcDir, { recursive: true }); - writeFileSync(join(srcDir, "Auth.swift"), "public class AuthClient {}\n"); - const result = parseSwiftProject(dir); - expect(result.symbols.map((s) => s.name)).toContain("AuthClient"); - }); -});