diff --git a/README.md b/README.md index 7b1a534..8a8b77d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Dead Code Hunter -A GitHub Action that finds unreachable functions in your codebase using [Supermodel](https://supermodeltools.com). +A GitHub Action that finds unused code in your codebase using [Supermodel](https://supermodeltools.com) static analysis. ## Installation @@ -45,6 +45,7 @@ That's it! The action will now analyze your code on every PR and comment with an | Input | Description | Required | Default | |-------|-------------|----------|---------| | `supermodel-api-key` | Your Supermodel API key | Yes | - | +| `github-token` | GitHub token for posting PR comments | No | `github.token` | | `comment-on-pr` | Post findings as PR comment | No | `true` | | `fail-on-dead-code` | Fail the action if dead code found | No | `false` | | `ignore-patterns` | JSON array of glob patterns to ignore | No | `[]` | @@ -61,36 +62,45 @@ That's it! The action will now analyze your code on every PR and comment with an ## What it does -1. Creates a zip of your repository -2. Sends it to Supermodel for analysis -3. Identifies functions with no callers -4. Filters out false positives (entry points, exports, tests) -5. Posts findings as a PR comment +1. Creates a zip of your repository via `git archive` +2. Sends it to the Supermodel dead code analysis API +3. The API performs symbol-level import analysis to identify unused exports +4. Results are returned with confidence levels and reasons +5. Posts findings as a PR comment with a sortable table + +## What it detects + +- **Functions** and **methods** with no callers +- **Classes** and **interfaces** that are never referenced +- **Types**, **variables**, and **constants** that are exported but never imported +- **Orphaned files** whose exports have no importers anywhere in the codebase +- **Transitively dead code** — code only called by other dead code + +Each finding includes a **confidence level** (high, medium, low) and a **reason** explaining why it was flagged. ## Example output > ## Dead Code Hunter > -> Found **3** potentially unused functions: +> Found **3** potentially unused code elements: +> +> | Name | Type | File | Line | Confidence | +> |------|------|------|------|------------| +> | `unusedHelper` | function | src/utils.ts#L42 | L42 | :red_circle: high | +> | `OldValidator` | class | src/validation.ts#L15 | L15 | :red_circle: high | +> | `LegacyConfig` | interface | src/legacy.ts#L8 | L8 | :orange_circle: medium | +> +>
Analysis summary > -> | Function | File | Line | -> |----------|------|------| -> | `unusedHelper` | src/utils.ts#L42 | L42 | -> | `oldValidator` | src/validation.ts#L15 | L15 | -> | `deprecatedFn` | src/legacy.ts#L8 | L8 | +> - **Total declarations analyzed**: 150 +> - **Dead code candidates**: 3 +> - **Alive code**: 147 +> - **Analysis method**: symbol_level_import_analysis +> +>
> > --- -> _Powered by [Supermodel](https://supermodeltools.com) graph analysis_ - -## False positive filtering - -The action automatically skips: - -- **Entry point files**: `index.ts`, `main.ts`, `app.ts` -- **Entry point functions**: `main`, `run`, `start`, `init`, `handler` -- **Exported functions**: May be called from outside the repo -- **Test files**: `*.test.ts`, `*.spec.ts`, `__tests__/**` -- **Build output**: `node_modules`, `dist`, `build`, `target` +> _Powered by [Supermodel](https://supermodeltools.com) dead code analysis_ ## Supported languages diff --git a/action.yml b/action.yml index 7c37c98..e0fc913 100644 --- a/action.yml +++ b/action.yml @@ -1,5 +1,5 @@ name: 'Dead Code Hunter' -description: 'Find unreachable functions in your codebase using Supermodel call graphs' +description: 'Find unused code in your codebase using Supermodel static analysis' author: 'Supermodel Tools' branding: @@ -29,9 +29,9 @@ inputs: outputs: dead-code-count: - description: 'Number of dead code functions found' + description: 'Number of unused code elements found' dead-code-json: - description: 'JSON array of dead code findings' + description: 'JSON array of dead code findings with type, confidence, and reason' runs: using: 'node20' diff --git a/dist/dead-code.d.ts b/dist/dead-code.d.ts index 3e5b129..2e2a7ca 100644 --- a/dist/dead-code.d.ts +++ b/dist/dead-code.d.ts @@ -1,50 +1,12 @@ -import { CodeGraphNode, CodeGraphRelationship } from '@supermodeltools/sdk'; +import type { DeadCodeCandidate, DeadCodeAnalysisResponse, DeadCodeAnalysisMetadata } from '@supermodeltools/sdk'; +export type { DeadCodeCandidate, DeadCodeAnalysisResponse, DeadCodeAnalysisMetadata }; /** - * Represents a potentially unused function found in the codebase. + * Filters dead code candidates by user-provided ignore patterns. + * The API handles all analysis server-side; this is purely for + * client-side post-filtering on file paths. */ -export interface DeadCodeResult { - id: string; - name: string; - filePath: string; - startLine?: number; - endLine?: number; -} -/** Default glob patterns for files to exclude from dead code analysis. */ -export declare const DEFAULT_EXCLUDE_PATTERNS: string[]; -/** Glob patterns for files that are considered entry points. */ -export declare const ENTRY_POINT_PATTERNS: string[]; -/** Function names that are considered entry points. */ -export declare const ENTRY_POINT_FUNCTION_NAMES: string[]; +export declare function filterByIgnorePatterns(candidates: DeadCodeCandidate[], ignorePatterns: string[]): DeadCodeCandidate[]; /** - * Checks if a file path matches any entry point pattern. - * @param filePath - The file path to check - * @returns True if the file is an entry point + * Formats dead code analysis results as a GitHub PR comment. */ -export declare function isEntryPointFile(filePath: string): boolean; -/** - * Checks if a function name is a common entry point name. - * @param name - The function name to check - * @returns True if the function name is an entry point - */ -export declare function isEntryPointFunction(name: string): boolean; -/** - * Checks if a file should be ignored based on exclude patterns. - * @param filePath - The file path to check - * @param ignorePatterns - Additional patterns to ignore - * @returns True if the file should be ignored - */ -export declare function shouldIgnoreFile(filePath: string, ignorePatterns?: string[]): boolean; -/** - * Analyzes a code graph to find functions that are never called. - * @param nodes - All nodes from the code graph - * @param relationships - All relationships from the code graph - * @param ignorePatterns - Additional glob patterns to ignore - * @returns Array of potentially unused functions - */ -export declare function findDeadCode(nodes: CodeGraphNode[], relationships: CodeGraphRelationship[], ignorePatterns?: string[]): DeadCodeResult[]; -/** - * Formats dead code results as a GitHub PR comment. - * @param deadCode - Array of dead code results - * @returns Markdown-formatted comment string - */ -export declare function formatPrComment(deadCode: DeadCodeResult[]): string; +export declare function formatPrComment(candidates: DeadCodeCandidate[], metadata?: DeadCodeAnalysisMetadata): string; diff --git a/dist/dead-code.d.ts.map b/dist/dead-code.d.ts.map index 987c001..18a350a 100644 --- a/dist/dead-code.d.ts.map +++ b/dist/dead-code.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"","sourceRoot":"","sources":["file:///Users/jag/dead-code-hunter/src/dead-code.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE5E;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,0EAA0E;AAC1E,eAAO,MAAM,wBAAwB,UAiBpC,CAAC;AAEF,gEAAgE;AAChE,eAAO,MAAM,oBAAoB,UAUhC,CAAC;AAEF,uDAAuD;AACvD,eAAO,MAAM,0BAA0B,UAUtC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAG1D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,GAAE,MAAM,EAAO,GAAG,OAAO,CAGzF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,aAAa,EAAE,EACtB,aAAa,EAAE,qBAAqB,EAAE,EACtC,cAAc,GAAE,MAAM,EAAO,GAC5B,cAAc,EAAE,CA6ClB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,MAAM,CAiClE"} \ No newline at end of file +{"version":3,"file":"","sourceRoot":"","sources":["file:///Users/jag/repos/dead-code-hunter-issue-8/src/dead-code.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAGlH,YAAY,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,CAAC;AAEtF;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,iBAAiB,EAAE,EAC/B,cAAc,EAAE,MAAM,EAAE,GACvB,iBAAiB,EAAE,CAGrB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,iBAAiB,EAAE,EAC/B,QAAQ,CAAC,EAAE,wBAAwB,GAClC,MAAM,CAgDR"} \ No newline at end of file diff --git a/dist/index.d.ts.map b/dist/index.d.ts.map index 650454b..93efa40 100644 --- a/dist/index.d.ts.map +++ b/dist/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"","sourceRoot":"","sources":["file:///Users/jag/dead-code-hunter/src/index.ts"],"names":[],"mappings":""} \ No newline at end of file +{"version":3,"file":"","sourceRoot":"","sources":["file:///Users/jag/repos/dead-code-hunter-issue-8/src/index.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index aba9b31..73d82ed 100644 --- a/dist/index.js +++ b/dist/index.js @@ -7161,7 +7161,7 @@ var request = withDefaults(import_endpoint.endpoint, { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -7230,7 +7230,7 @@ class DefaultApi extends runtime.BaseAPI { query: queryParameters, body: formParams, }, initOverrides); - return new runtime.JSONApiResponse(response, (jsonValue) => (0, index_1.CodeGraphEnvelopeFromJSON)(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => (0, index_1.CodeGraphEnvelopeAsyncFromJSON)(jsonValue)); }); } /** @@ -7243,6 +7243,64 @@ class DefaultApi extends runtime.BaseAPI { return yield response.value(); }); } + /** + * Upload a zipped repository snapshot to identify dead (unreachable) code candidates by combining parse graph declarations with call graph relationships. + * Dead code analysis + */ + generateDeadCodeAnalysisRaw(requestParameters, initOverrides) { + return __awaiter(this, void 0, void 0, function* () { + if (requestParameters['idempotencyKey'] == null) { + throw new runtime.RequiredError('idempotencyKey', 'Required parameter "idempotencyKey" was null or undefined when calling generateDeadCodeAnalysis().'); + } + if (requestParameters['file'] == null) { + throw new runtime.RequiredError('file', 'Required parameter "file" was null or undefined when calling generateDeadCodeAnalysis().'); + } + const queryParameters = {}; + const headerParameters = {}; + if (requestParameters['idempotencyKey'] != null) { + headerParameters['Idempotency-Key'] = String(requestParameters['idempotencyKey']); + } + if (this.configuration && this.configuration.apiKey) { + headerParameters["X-Api-Key"] = yield this.configuration.apiKey("X-Api-Key"); // ApiKeyAuth authentication + } + const consumes = [ + { contentType: 'multipart/form-data' }, + ]; + // @ts-ignore: canConsumeForm may be unused + const canConsumeForm = runtime.canConsumeForm(consumes); + let formParams; + let useForm = false; + // use FormData to transmit files using content-type "multipart/form-data" + useForm = canConsumeForm; + if (useForm) { + formParams = new FormData(); + } + else { + formParams = new URLSearchParams(); + } + if (requestParameters['file'] != null) { + formParams.append('file', requestParameters['file']); + } + const response = yield this.request({ + path: `/v1/analysis/dead-code`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: formParams, + }, initOverrides); + return new runtime.JSONApiResponse(response, (jsonValue) => (0, index_1.DeadCodeAnalysisResponseAsyncFromJSON)(jsonValue)); + }); + } + /** + * Upload a zipped repository snapshot to identify dead (unreachable) code candidates by combining parse graph declarations with call graph relationships. + * Dead code analysis + */ + generateDeadCodeAnalysis(requestParameters, initOverrides) { + return __awaiter(this, void 0, void 0, function* () { + const response = yield this.generateDeadCodeAnalysisRaw(requestParameters, initOverrides); + return yield response.value(); + }); + } /** * Upload a zipped repository snapshot to generate the dependency graph. * Dependency graph @@ -7288,7 +7346,7 @@ class DefaultApi extends runtime.BaseAPI { query: queryParameters, body: formParams, }, initOverrides); - return new runtime.JSONApiResponse(response, (jsonValue) => (0, index_1.CodeGraphEnvelopeFromJSON)(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => (0, index_1.CodeGraphEnvelopeAsyncFromJSON)(jsonValue)); }); } /** @@ -7346,7 +7404,7 @@ class DefaultApi extends runtime.BaseAPI { query: queryParameters, body: formParams, }, initOverrides); - return new runtime.JSONApiResponse(response, (jsonValue) => (0, index_1.DomainClassificationResponseFromJSON)(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => (0, index_1.DomainClassificationResponseAsyncFromJSON)(jsonValue)); }); } /** @@ -7404,7 +7462,7 @@ class DefaultApi extends runtime.BaseAPI { query: queryParameters, body: formParams, }, initOverrides); - return new runtime.JSONApiResponse(response, (jsonValue) => (0, index_1.CodeGraphEnvelopeFromJSON)(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => (0, index_1.CodeGraphEnvelopeAsyncFromJSON)(jsonValue)); }); } /** @@ -7462,7 +7520,7 @@ class DefaultApi extends runtime.BaseAPI { query: queryParameters, body: formParams, }, initOverrides); - return new runtime.JSONApiResponse(response, (jsonValue) => (0, index_1.SupermodelIRFromJSON)(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => (0, index_1.SupermodelIRAsyncFromJSON)(jsonValue)); }); } /** @@ -7506,6 +7564,249 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); __exportStar(__nccwpck_require__(1464), exports); +/***/ }), + +/***/ 1277: +/***/ (function(__unused_webpack_module, exports) { + +"use strict"; + +/** + * Async Client Wrapper for Supermodel API + * + * Provides automatic polling for async job endpoints, so you can use them + * like synchronous APIs without manually implementing polling loops. + * + * @example + * ```typescript + * import { DefaultApi, Configuration, SupermodelClient } from '@supermodeltools/sdk'; + * + * const api = new DefaultApi(new Configuration({ + * basePath: 'https://api.supermodel.tools', + * apiKey: () => 'your-api-key' + * })); + * + * const client = new SupermodelClient(api); + * + * // Returns the unwrapped result - polling is automatic! + * const graph = await client.generateDependencyGraph(zipFile); + * console.log(graph.graph.nodes.length); + * ``` + */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SupermodelClient = exports.PollingTimeoutError = exports.JobFailedError = void 0; +/** + * Error thrown when a job fails. + */ +class JobFailedError extends Error { + constructor(jobId, errorMessage) { + super(`Job ${jobId} failed: ${errorMessage}`); + this.jobId = jobId; + this.errorMessage = errorMessage; + this.name = 'JobFailedError'; + } +} +exports.JobFailedError = JobFailedError; +/** + * Error thrown when polling times out. + */ +class PollingTimeoutError extends Error { + constructor(jobId, timeoutMs, attempts) { + super(`Polling timed out for job ${jobId} after ${timeoutMs}ms (${attempts} attempts)`); + this.jobId = jobId; + this.timeoutMs = timeoutMs; + this.attempts = attempts; + this.name = 'PollingTimeoutError'; + } +} +exports.PollingTimeoutError = PollingTimeoutError; +/** + * Default idempotency key generator. + */ +function defaultGenerateIdempotencyKey() { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} +function sleepWithAbort(ms, signal) { + return new Promise((resolve, reject) => { + if (signal === null || signal === void 0 ? void 0 : signal.aborted) { + const error = new Error('Polling aborted'); + error.name = 'AbortError'; + reject(error); + return; + } + const timeout = setTimeout(() => { + if (signal && onAbort) { + signal.removeEventListener('abort', onAbort); + } + resolve(); + }, ms); + let onAbort; + if (signal) { + onAbort = () => { + clearTimeout(timeout); + signal.removeEventListener('abort', onAbort); + const error = new Error('Polling aborted'); + error.name = 'AbortError'; + reject(error); + }; + signal.addEventListener('abort', onAbort); + } + }); +} +/** + * Poll an async endpoint until completion. + */ +function pollUntilComplete(apiCall, options) { + return __awaiter(this, void 0, void 0, function* () { + const { timeoutMs = 900000, defaultRetryIntervalMs = 10000, maxPollingAttempts = 90, onPollingProgress, signal, } = options; + const startTime = Date.now(); + let attempt = 0; + let jobId = ''; + while (attempt < maxPollingAttempts) { + // Check for abort before each attempt + if (signal === null || signal === void 0 ? void 0 : signal.aborted) { + const error = new Error('Polling aborted'); + error.name = 'AbortError'; + throw error; + } + attempt++; + const elapsedMs = Date.now() - startTime; + if (elapsedMs >= timeoutMs) { + throw new PollingTimeoutError(jobId || 'unknown', timeoutMs, attempt); + } + const response = yield apiCall(); + jobId = response.jobId; + const status = response.status; + if (onPollingProgress) { + const nextRetryMs = status === 'completed' || status === 'failed' + ? undefined + : (response.retryAfter || defaultRetryIntervalMs / 1000) * 1000; + onPollingProgress({ + jobId, + status, + attempt, + maxAttempts: maxPollingAttempts, + elapsedMs, + nextRetryMs, + }); + } + if (status === 'completed') { + if (response.result !== undefined) { + return response.result; + } + throw new Error(`Job ${jobId} completed but result is undefined`); + } + if (status === 'failed') { + throw new JobFailedError(jobId, response.error || 'Unknown error'); + } + const retryAfterMs = (response.retryAfter || defaultRetryIntervalMs / 1000) * 1000; + // Use abortable sleep + yield sleepWithAbort(retryAfterMs, signal); + } + throw new PollingTimeoutError(jobId || 'unknown', timeoutMs, attempt); + }); +} +/** + * Async client wrapper that handles polling automatically. + * + * Wraps the generated DefaultApi and provides simplified methods + * for graph generation that handle async job polling internally. + */ +class SupermodelClient { + constructor(api, options = {}) { + this.api = api; + this.options = options; + this.generateIdempotencyKey = options.generateIdempotencyKey || defaultGenerateIdempotencyKey; + } + /** + * Generate a dependency graph from a zip file. + * Automatically handles polling until the job completes. + * + * @param file - Zip file containing the repository + * @param options - Optional request options + * @returns The dependency graph result + */ + generateDependencyGraph(file, options) { + return __awaiter(this, void 0, void 0, function* () { + const key = (options === null || options === void 0 ? void 0 : options.idempotencyKey) || this.generateIdempotencyKey(); + const pollOptions = (options === null || options === void 0 ? void 0 : options.signal) ? Object.assign(Object.assign({}, this.options), { signal: options.signal }) : this.options; + return pollUntilComplete(() => this.api.generateDependencyGraph({ idempotencyKey: key, file }, options === null || options === void 0 ? void 0 : options.initOverrides), pollOptions); + }); + } + /** + * Generate a call graph from a zip file. + * Automatically handles polling until the job completes. + */ + generateCallGraph(file, options) { + return __awaiter(this, void 0, void 0, function* () { + const key = (options === null || options === void 0 ? void 0 : options.idempotencyKey) || this.generateIdempotencyKey(); + const pollOptions = (options === null || options === void 0 ? void 0 : options.signal) ? Object.assign(Object.assign({}, this.options), { signal: options.signal }) : this.options; + return pollUntilComplete(() => this.api.generateCallGraph({ idempotencyKey: key, file }, options === null || options === void 0 ? void 0 : options.initOverrides), pollOptions); + }); + } + /** + * Generate a domain graph from a zip file. + * Automatically handles polling until the job completes. + */ + generateDomainGraph(file, options) { + return __awaiter(this, void 0, void 0, function* () { + const key = (options === null || options === void 0 ? void 0 : options.idempotencyKey) || this.generateIdempotencyKey(); + const pollOptions = (options === null || options === void 0 ? void 0 : options.signal) ? Object.assign(Object.assign({}, this.options), { signal: options.signal }) : this.options; + return pollUntilComplete(() => this.api.generateDomainGraph({ idempotencyKey: key, file }, options === null || options === void 0 ? void 0 : options.initOverrides), pollOptions); + }); + } + /** + * Generate a parse graph from a zip file. + * Automatically handles polling until the job completes. + */ + generateParseGraph(file, options) { + return __awaiter(this, void 0, void 0, function* () { + const key = (options === null || options === void 0 ? void 0 : options.idempotencyKey) || this.generateIdempotencyKey(); + const pollOptions = (options === null || options === void 0 ? void 0 : options.signal) ? Object.assign(Object.assign({}, this.options), { signal: options.signal }) : this.options; + return pollUntilComplete(() => this.api.generateParseGraph({ idempotencyKey: key, file }, options === null || options === void 0 ? void 0 : options.initOverrides), pollOptions); + }); + } + /** + * Generate a Supermodel IR from a zip file. + * Automatically handles polling until the job completes. + */ + generateSupermodelGraph(file, options) { + return __awaiter(this, void 0, void 0, function* () { + const key = (options === null || options === void 0 ? void 0 : options.idempotencyKey) || this.generateIdempotencyKey(); + const pollOptions = (options === null || options === void 0 ? void 0 : options.signal) ? Object.assign(Object.assign({}, this.options), { signal: options.signal }) : this.options; + return pollUntilComplete(() => this.api.generateSupermodelGraph({ idempotencyKey: key, file }, options === null || options === void 0 ? void 0 : options.initOverrides), pollOptions); + }); + } + /** + * Access the underlying raw API for methods that don't need polling + * or when you want direct control over the async envelope responses. + */ + get rawApi() { + return this.api; + } +} +exports.SupermodelClient = SupermodelClient; + + /***/ }), /***/ 6381: @@ -7533,6 +7834,90 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); __exportStar(__nccwpck_require__(6361), exports); __exportStar(__nccwpck_require__(8415), exports); __exportStar(__nccwpck_require__(3056), exports); +__exportStar(__nccwpck_require__(1277), exports); + + +/***/ }), + +/***/ 5331: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +/* tslint:disable */ +/* eslint-disable */ +/** + * Supermodel + * Code Graphing & Analysis API + * + * The version of the OpenAPI document: 0.9.3 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.AliveCodeItemTypeEnum = void 0; +exports.instanceOfAliveCodeItem = instanceOfAliveCodeItem; +exports.AliveCodeItemFromJSON = AliveCodeItemFromJSON; +exports.AliveCodeItemFromJSONTyped = AliveCodeItemFromJSONTyped; +exports.AliveCodeItemToJSON = AliveCodeItemToJSON; +/** + * @export + */ +exports.AliveCodeItemTypeEnum = { + Function: 'function', + Class: 'class', + Method: 'method', + Interface: 'interface', + Type: 'type', + Variable: 'variable', + Constant: 'constant' +}; +/** + * Check if a given object implements the AliveCodeItem interface. + */ +function instanceOfAliveCodeItem(value) { + if (!('file' in value) || value['file'] === undefined) + return false; + if (!('name' in value) || value['name'] === undefined) + return false; + if (!('line' in value) || value['line'] === undefined) + return false; + if (!('type' in value) || value['type'] === undefined) + return false; + if (!('callerCount' in value) || value['callerCount'] === undefined) + return false; + return true; +} +function AliveCodeItemFromJSON(json) { + return AliveCodeItemFromJSONTyped(json, false); +} +function AliveCodeItemFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'file': json['file'], + 'name': json['name'], + 'line': json['line'], + 'type': json['type'], + 'callerCount': json['callerCount'], + }; +} +function AliveCodeItemToJSON(value) { + if (value == null) { + return value; + } + return { + 'file': value['file'], + 'name': value['name'], + 'line': value['line'], + 'type': value['type'], + 'callerCount': value['callerCount'], + }; +} /***/ }), @@ -7548,7 +7933,7 @@ __exportStar(__nccwpck_require__(3056), exports); * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -7564,10 +7949,24 @@ exports.ClassificationStatsToJSON = ClassificationStatsToJSON; * Check if a given object implements the ClassificationStats interface. */ function instanceOfClassificationStats(value) { - if (!('domainCount' in value) || value['domainCount'] === undefined) + if (!('nodeCount' in value) || value['nodeCount'] === undefined) return false; if (!('relationshipCount' in value) || value['relationshipCount'] === undefined) return false; + if (!('nodeTypes' in value) || value['nodeTypes'] === undefined) + return false; + if (!('relationshipTypes' in value) || value['relationshipTypes'] === undefined) + return false; + if (!('domainCount' in value) || value['domainCount'] === undefined) + return false; + if (!('subdomainCount' in value) || value['subdomainCount'] === undefined) + return false; + if (!('assignedFileCount' in value) || value['assignedFileCount'] === undefined) + return false; + if (!('assignedFunctionCount' in value) || value['assignedFunctionCount'] === undefined) + return false; + if (!('assignedClassCount' in value) || value['assignedClassCount'] === undefined) + return false; if (!('fileAssignments' in value) || value['fileAssignments'] === undefined) return false; if (!('functionAssignments' in value) || value['functionAssignments'] === undefined) @@ -7586,8 +7985,15 @@ function ClassificationStatsFromJSONTyped(json, ignoreDiscriminator) { return json; } return { - 'domainCount': json['domainCount'], + 'nodeCount': json['nodeCount'], 'relationshipCount': json['relationshipCount'], + 'nodeTypes': json['nodeTypes'], + 'relationshipTypes': json['relationshipTypes'], + 'domainCount': json['domainCount'], + 'subdomainCount': json['subdomainCount'], + 'assignedFileCount': json['assignedFileCount'], + 'assignedFunctionCount': json['assignedFunctionCount'], + 'assignedClassCount': json['assignedClassCount'], 'fileAssignments': json['fileAssignments'], 'functionAssignments': json['functionAssignments'], 'unassignedFunctions': json['unassignedFunctions'], @@ -7599,8 +8005,15 @@ function ClassificationStatsToJSON(value) { return value; } return { - 'domainCount': value['domainCount'], + 'nodeCount': value['nodeCount'], 'relationshipCount': value['relationshipCount'], + 'nodeTypes': value['nodeTypes'], + 'relationshipTypes': value['relationshipTypes'], + 'domainCount': value['domainCount'], + 'subdomainCount': value['subdomainCount'], + 'assignedFileCount': value['assignedFileCount'], + 'assignedFunctionCount': value['assignedFunctionCount'], + 'assignedClassCount': value['assignedClassCount'], 'fileAssignments': value['fileAssignments'], 'functionAssignments': value['functionAssignments'], 'unassignedFunctions': value['unassignedFunctions'], @@ -7622,7 +8035,7 @@ function ClassificationStatsToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -7636,6 +8049,7 @@ exports.CodeGraphEnvelopeFromJSONTyped = CodeGraphEnvelopeFromJSONTyped; exports.CodeGraphEnvelopeToJSON = CodeGraphEnvelopeToJSON; const CodeGraphStats_1 = __nccwpck_require__(2208); const CodeGraphEnvelopeGraph_1 = __nccwpck_require__(727); +const CodeGraphEnvelopeMetadata_1 = __nccwpck_require__(7122); /** * Check if a given object implements the CodeGraphEnvelope interface. */ @@ -7655,6 +8069,7 @@ function CodeGraphEnvelopeFromJSONTyped(json, ignoreDiscriminator) { 'generatedAt': json['generatedAt'] == null ? undefined : (new Date(json['generatedAt'])), 'message': json['message'] == null ? undefined : json['message'], 'stats': json['stats'] == null ? undefined : (0, CodeGraphStats_1.CodeGraphStatsFromJSON)(json['stats']), + 'metadata': json['metadata'] == null ? undefined : (0, CodeGraphEnvelopeMetadata_1.CodeGraphEnvelopeMetadataFromJSON)(json['metadata']), 'graph': (0, CodeGraphEnvelopeGraph_1.CodeGraphEnvelopeGraphFromJSON)(json['graph']), }; } @@ -7666,11 +8081,87 @@ function CodeGraphEnvelopeToJSON(value) { 'generatedAt': value['generatedAt'] == null ? undefined : ((value['generatedAt']).toISOString()), 'message': value['message'], 'stats': (0, CodeGraphStats_1.CodeGraphStatsToJSON)(value['stats']), + 'metadata': (0, CodeGraphEnvelopeMetadata_1.CodeGraphEnvelopeMetadataToJSON)(value['metadata']), 'graph': (0, CodeGraphEnvelopeGraph_1.CodeGraphEnvelopeGraphToJSON)(value['graph']), }; } +/***/ }), + +/***/ 8711: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +/* tslint:disable */ +/* eslint-disable */ +/** + * Supermodel + * Code Graphing & Analysis API + * + * The version of the OpenAPI document: 0.9.3 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.CodeGraphEnvelopeAsyncStatusEnum = void 0; +exports.instanceOfCodeGraphEnvelopeAsync = instanceOfCodeGraphEnvelopeAsync; +exports.CodeGraphEnvelopeAsyncFromJSON = CodeGraphEnvelopeAsyncFromJSON; +exports.CodeGraphEnvelopeAsyncFromJSONTyped = CodeGraphEnvelopeAsyncFromJSONTyped; +exports.CodeGraphEnvelopeAsyncToJSON = CodeGraphEnvelopeAsyncToJSON; +const CodeGraphEnvelope_1 = __nccwpck_require__(9995); +/** + * @export + */ +exports.CodeGraphEnvelopeAsyncStatusEnum = { + Pending: 'pending', + Processing: 'processing', + Completed: 'completed', + Failed: 'failed' +}; +/** + * Check if a given object implements the CodeGraphEnvelopeAsync interface. + */ +function instanceOfCodeGraphEnvelopeAsync(value) { + if (!('status' in value) || value['status'] === undefined) + return false; + if (!('jobId' in value) || value['jobId'] === undefined) + return false; + return true; +} +function CodeGraphEnvelopeAsyncFromJSON(json) { + return CodeGraphEnvelopeAsyncFromJSONTyped(json, false); +} +function CodeGraphEnvelopeAsyncFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'status': json['status'], + 'jobId': json['jobId'], + 'retryAfter': json['retryAfter'] == null ? undefined : json['retryAfter'], + 'error': json['error'] == null ? undefined : json['error'], + 'result': json['result'] == null ? undefined : (0, CodeGraphEnvelope_1.CodeGraphEnvelopeFromJSON)(json['result']), + }; +} +function CodeGraphEnvelopeAsyncToJSON(value) { + if (value == null) { + return value; + } + return { + 'status': value['status'], + 'jobId': value['jobId'], + 'retryAfter': value['retryAfter'], + 'error': value['error'], + 'result': (0, CodeGraphEnvelope_1.CodeGraphEnvelopeToJSON)(value['result']), + }; +} + + /***/ }), /***/ 727: @@ -7684,7 +8175,7 @@ function CodeGraphEnvelopeToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -7733,7 +8224,7 @@ function CodeGraphEnvelopeGraphToJSON(value) { /***/ }), -/***/ 1811: +/***/ 7122: /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -7744,7 +8235,7 @@ function CodeGraphEnvelopeGraphToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -7752,9 +8243,67 @@ function CodeGraphEnvelopeGraphToJSON(value) { * Do not edit the class manually. */ Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.instanceOfCodeGraphNode = instanceOfCodeGraphNode; -exports.CodeGraphNodeFromJSON = CodeGraphNodeFromJSON; -exports.CodeGraphNodeFromJSONTyped = CodeGraphNodeFromJSONTyped; +exports.instanceOfCodeGraphEnvelopeMetadata = instanceOfCodeGraphEnvelopeMetadata; +exports.CodeGraphEnvelopeMetadataFromJSON = CodeGraphEnvelopeMetadataFromJSON; +exports.CodeGraphEnvelopeMetadataFromJSONTyped = CodeGraphEnvelopeMetadataFromJSONTyped; +exports.CodeGraphEnvelopeMetadataToJSON = CodeGraphEnvelopeMetadataToJSON; +/** + * Check if a given object implements the CodeGraphEnvelopeMetadata interface. + */ +function instanceOfCodeGraphEnvelopeMetadata(value) { + return true; +} +function CodeGraphEnvelopeMetadataFromJSON(json) { + return CodeGraphEnvelopeMetadataFromJSONTyped(json, false); +} +function CodeGraphEnvelopeMetadataFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'analysisStartTime': json['analysisStartTime'] == null ? undefined : (new Date(json['analysisStartTime'])), + 'analysisEndTime': json['analysisEndTime'] == null ? undefined : (new Date(json['analysisEndTime'])), + 'fileCount': json['fileCount'] == null ? undefined : json['fileCount'], + 'languages': json['languages'] == null ? undefined : json['languages'], + }; +} +function CodeGraphEnvelopeMetadataToJSON(value) { + if (value == null) { + return value; + } + return { + 'analysisStartTime': value['analysisStartTime'] == null ? undefined : ((value['analysisStartTime']).toISOString()), + 'analysisEndTime': value['analysisEndTime'] == null ? undefined : ((value['analysisEndTime']).toISOString()), + 'fileCount': value['fileCount'], + 'languages': value['languages'], + }; +} + + +/***/ }), + +/***/ 1811: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +/* tslint:disable */ +/* eslint-disable */ +/** + * Supermodel + * Code Graphing & Analysis API + * + * The version of the OpenAPI document: 0.9.3 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.instanceOfCodeGraphNode = instanceOfCodeGraphNode; +exports.CodeGraphNodeFromJSON = CodeGraphNodeFromJSON; +exports.CodeGraphNodeFromJSONTyped = CodeGraphNodeFromJSONTyped; exports.CodeGraphNodeToJSON = CodeGraphNodeToJSON; /** * Check if a given object implements the CodeGraphNode interface. @@ -7802,7 +8351,7 @@ function CodeGraphNodeToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -7870,7 +8419,7 @@ function CodeGraphRelationshipToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -7896,6 +8445,10 @@ function CodeGraphStatsFromJSONTyped(json, ignoreDiscriminator) { return json; } return { + 'nodeCount': json['nodeCount'] == null ? undefined : json['nodeCount'], + 'relationshipCount': json['relationshipCount'] == null ? undefined : json['relationshipCount'], + 'nodeTypes': json['nodeTypes'] == null ? undefined : json['nodeTypes'], + 'relationshipTypes': json['relationshipTypes'] == null ? undefined : json['relationshipTypes'], 'filesProcessed': json['filesProcessed'] == null ? undefined : json['filesProcessed'], 'classes': json['classes'] == null ? undefined : json['classes'], 'functions': json['functions'] == null ? undefined : json['functions'], @@ -7908,6 +8461,10 @@ function CodeGraphStatsToJSON(value) { return value; } return { + 'nodeCount': value['nodeCount'], + 'relationshipCount': value['relationshipCount'], + 'nodeTypes': value['nodeTypes'], + 'relationshipTypes': value['relationshipTypes'], 'filesProcessed': value['filesProcessed'], 'classes': value['classes'], 'functions': value['functions'], @@ -7917,6 +8474,322 @@ function CodeGraphStatsToJSON(value) { } +/***/ }), + +/***/ 8386: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +/* tslint:disable */ +/* eslint-disable */ +/** + * Supermodel + * Code Graphing & Analysis API + * + * The version of the OpenAPI document: 0.9.3 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.instanceOfDeadCodeAnalysisMetadata = instanceOfDeadCodeAnalysisMetadata; +exports.DeadCodeAnalysisMetadataFromJSON = DeadCodeAnalysisMetadataFromJSON; +exports.DeadCodeAnalysisMetadataFromJSONTyped = DeadCodeAnalysisMetadataFromJSONTyped; +exports.DeadCodeAnalysisMetadataToJSON = DeadCodeAnalysisMetadataToJSON; +/** + * Check if a given object implements the DeadCodeAnalysisMetadata interface. + */ +function instanceOfDeadCodeAnalysisMetadata(value) { + if (!('totalDeclarations' in value) || value['totalDeclarations'] === undefined) + return false; + if (!('deadCodeCandidates' in value) || value['deadCodeCandidates'] === undefined) + return false; + if (!('aliveCode' in value) || value['aliveCode'] === undefined) + return false; + if (!('analysisMethod' in value) || value['analysisMethod'] === undefined) + return false; + return true; +} +function DeadCodeAnalysisMetadataFromJSON(json) { + return DeadCodeAnalysisMetadataFromJSONTyped(json, false); +} +function DeadCodeAnalysisMetadataFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'totalDeclarations': json['totalDeclarations'], + 'deadCodeCandidates': json['deadCodeCandidates'], + 'aliveCode': json['aliveCode'], + 'rootFilesCount': json['rootFilesCount'] == null ? undefined : json['rootFilesCount'], + 'transitiveDeadCount': json['transitiveDeadCount'] == null ? undefined : json['transitiveDeadCount'], + 'symbolLevelDeadCount': json['symbolLevelDeadCount'] == null ? undefined : json['symbolLevelDeadCount'], + 'analysisMethod': json['analysisMethod'], + 'analysisStartTime': json['analysisStartTime'] == null ? undefined : (new Date(json['analysisStartTime'])), + 'analysisEndTime': json['analysisEndTime'] == null ? undefined : (new Date(json['analysisEndTime'])), + }; +} +function DeadCodeAnalysisMetadataToJSON(value) { + if (value == null) { + return value; + } + return { + 'totalDeclarations': value['totalDeclarations'], + 'deadCodeCandidates': value['deadCodeCandidates'], + 'aliveCode': value['aliveCode'], + 'rootFilesCount': value['rootFilesCount'], + 'transitiveDeadCount': value['transitiveDeadCount'], + 'symbolLevelDeadCount': value['symbolLevelDeadCount'], + 'analysisMethod': value['analysisMethod'], + 'analysisStartTime': value['analysisStartTime'] == null ? undefined : ((value['analysisStartTime']).toISOString()), + 'analysisEndTime': value['analysisEndTime'] == null ? undefined : ((value['analysisEndTime']).toISOString()), + }; +} + + +/***/ }), + +/***/ 536: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +/* tslint:disable */ +/* eslint-disable */ +/** + * Supermodel + * Code Graphing & Analysis API + * + * The version of the OpenAPI document: 0.9.3 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.instanceOfDeadCodeAnalysisResponse = instanceOfDeadCodeAnalysisResponse; +exports.DeadCodeAnalysisResponseFromJSON = DeadCodeAnalysisResponseFromJSON; +exports.DeadCodeAnalysisResponseFromJSONTyped = DeadCodeAnalysisResponseFromJSONTyped; +exports.DeadCodeAnalysisResponseToJSON = DeadCodeAnalysisResponseToJSON; +const DeadCodeAnalysisMetadata_1 = __nccwpck_require__(8386); +const DeadCodeCandidate_1 = __nccwpck_require__(4196); +const EntryPoint_1 = __nccwpck_require__(8512); +const AliveCodeItem_1 = __nccwpck_require__(5331); +/** + * Check if a given object implements the DeadCodeAnalysisResponse interface. + */ +function instanceOfDeadCodeAnalysisResponse(value) { + if (!('metadata' in value) || value['metadata'] === undefined) + return false; + if (!('deadCodeCandidates' in value) || value['deadCodeCandidates'] === undefined) + return false; + if (!('aliveCode' in value) || value['aliveCode'] === undefined) + return false; + if (!('entryPoints' in value) || value['entryPoints'] === undefined) + return false; + return true; +} +function DeadCodeAnalysisResponseFromJSON(json) { + return DeadCodeAnalysisResponseFromJSONTyped(json, false); +} +function DeadCodeAnalysisResponseFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'metadata': (0, DeadCodeAnalysisMetadata_1.DeadCodeAnalysisMetadataFromJSON)(json['metadata']), + 'deadCodeCandidates': (json['deadCodeCandidates'].map(DeadCodeCandidate_1.DeadCodeCandidateFromJSON)), + 'aliveCode': (json['aliveCode'].map(AliveCodeItem_1.AliveCodeItemFromJSON)), + 'entryPoints': (json['entryPoints'].map(EntryPoint_1.EntryPointFromJSON)), + }; +} +function DeadCodeAnalysisResponseToJSON(value) { + if (value == null) { + return value; + } + return { + 'metadata': (0, DeadCodeAnalysisMetadata_1.DeadCodeAnalysisMetadataToJSON)(value['metadata']), + 'deadCodeCandidates': (value['deadCodeCandidates'].map(DeadCodeCandidate_1.DeadCodeCandidateToJSON)), + 'aliveCode': (value['aliveCode'].map(AliveCodeItem_1.AliveCodeItemToJSON)), + 'entryPoints': (value['entryPoints'].map(EntryPoint_1.EntryPointToJSON)), + }; +} + + +/***/ }), + +/***/ 3750: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +/* tslint:disable */ +/* eslint-disable */ +/** + * Supermodel + * Code Graphing & Analysis API + * + * The version of the OpenAPI document: 0.9.3 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DeadCodeAnalysisResponseAsyncStatusEnum = void 0; +exports.instanceOfDeadCodeAnalysisResponseAsync = instanceOfDeadCodeAnalysisResponseAsync; +exports.DeadCodeAnalysisResponseAsyncFromJSON = DeadCodeAnalysisResponseAsyncFromJSON; +exports.DeadCodeAnalysisResponseAsyncFromJSONTyped = DeadCodeAnalysisResponseAsyncFromJSONTyped; +exports.DeadCodeAnalysisResponseAsyncToJSON = DeadCodeAnalysisResponseAsyncToJSON; +const DeadCodeAnalysisResponse_1 = __nccwpck_require__(536); +/** + * @export + */ +exports.DeadCodeAnalysisResponseAsyncStatusEnum = { + Pending: 'pending', + Processing: 'processing', + Completed: 'completed', + Failed: 'failed' +}; +/** + * Check if a given object implements the DeadCodeAnalysisResponseAsync interface. + */ +function instanceOfDeadCodeAnalysisResponseAsync(value) { + if (!('status' in value) || value['status'] === undefined) + return false; + if (!('jobId' in value) || value['jobId'] === undefined) + return false; + return true; +} +function DeadCodeAnalysisResponseAsyncFromJSON(json) { + return DeadCodeAnalysisResponseAsyncFromJSONTyped(json, false); +} +function DeadCodeAnalysisResponseAsyncFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'status': json['status'], + 'jobId': json['jobId'], + 'retryAfter': json['retryAfter'] == null ? undefined : json['retryAfter'], + 'error': json['error'] == null ? undefined : json['error'], + 'result': json['result'] == null ? undefined : (0, DeadCodeAnalysisResponse_1.DeadCodeAnalysisResponseFromJSON)(json['result']), + }; +} +function DeadCodeAnalysisResponseAsyncToJSON(value) { + if (value == null) { + return value; + } + return { + 'status': value['status'], + 'jobId': value['jobId'], + 'retryAfter': value['retryAfter'], + 'error': value['error'], + 'result': (0, DeadCodeAnalysisResponse_1.DeadCodeAnalysisResponseToJSON)(value['result']), + }; +} + + +/***/ }), + +/***/ 4196: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +/* tslint:disable */ +/* eslint-disable */ +/** + * Supermodel + * Code Graphing & Analysis API + * + * The version of the OpenAPI document: 0.9.3 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DeadCodeCandidateConfidenceEnum = exports.DeadCodeCandidateTypeEnum = void 0; +exports.instanceOfDeadCodeCandidate = instanceOfDeadCodeCandidate; +exports.DeadCodeCandidateFromJSON = DeadCodeCandidateFromJSON; +exports.DeadCodeCandidateFromJSONTyped = DeadCodeCandidateFromJSONTyped; +exports.DeadCodeCandidateToJSON = DeadCodeCandidateToJSON; +/** + * @export + */ +exports.DeadCodeCandidateTypeEnum = { + Function: 'function', + Class: 'class', + Method: 'method', + Interface: 'interface', + Type: 'type', + Variable: 'variable', + Constant: 'constant' +}; +/** + * @export + */ +exports.DeadCodeCandidateConfidenceEnum = { + High: 'high', + Medium: 'medium', + Low: 'low' +}; +/** + * Check if a given object implements the DeadCodeCandidate interface. + */ +function instanceOfDeadCodeCandidate(value) { + if (!('file' in value) || value['file'] === undefined) + return false; + if (!('name' in value) || value['name'] === undefined) + return false; + if (!('line' in value) || value['line'] === undefined) + return false; + if (!('type' in value) || value['type'] === undefined) + return false; + if (!('confidence' in value) || value['confidence'] === undefined) + return false; + if (!('reason' in value) || value['reason'] === undefined) + return false; + return true; +} +function DeadCodeCandidateFromJSON(json) { + return DeadCodeCandidateFromJSONTyped(json, false); +} +function DeadCodeCandidateFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'file': json['file'], + 'name': json['name'], + 'line': json['line'], + 'type': json['type'], + 'confidence': json['confidence'], + 'reason': json['reason'], + }; +} +function DeadCodeCandidateToJSON(value) { + if (value == null) { + return value; + } + return { + 'file': value['file'], + 'name': value['name'], + 'line': value['line'], + 'type': value['type'], + 'confidence': value['confidence'], + 'reason': value['reason'], + }; +} + + /***/ }), /***/ 4181: @@ -7930,7 +8803,7 @@ function CodeGraphStatsToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -7988,7 +8861,7 @@ function DomainClassAssignmentToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -8008,12 +8881,18 @@ const FunctionDescription_1 = __nccwpck_require__(8942); const DomainRelationship_1 = __nccwpck_require__(1988); const DomainSummary_1 = __nccwpck_require__(7228); const ClassificationStats_1 = __nccwpck_require__(6367); +const DomainClassificationResponseGraph_1 = __nccwpck_require__(9625); +const CodeGraphEnvelopeMetadata_1 = __nccwpck_require__(7122); /** * Check if a given object implements the DomainClassificationResponse interface. */ function instanceOfDomainClassificationResponse(value) { if (!('runId' in value) || value['runId'] === undefined) return false; + if (!('graph' in value) || value['graph'] === undefined) + return false; + if (!('metadata' in value) || value['metadata'] === undefined) + return false; if (!('domains' in value) || value['domains'] === undefined) return false; if (!('relationships' in value) || value['relationships'] === undefined) @@ -8039,6 +8918,8 @@ function DomainClassificationResponseFromJSONTyped(json, ignoreDiscriminator) { } return { 'runId': json['runId'], + 'graph': (0, DomainClassificationResponseGraph_1.DomainClassificationResponseGraphFromJSON)(json['graph']), + 'metadata': (0, CodeGraphEnvelopeMetadata_1.CodeGraphEnvelopeMetadataFromJSON)(json['metadata']), 'domains': (json['domains'].map(DomainSummary_1.DomainSummaryFromJSON)), 'relationships': (json['relationships'].map(DomainRelationship_1.DomainRelationshipFromJSON)), 'fileAssignments': (json['fileAssignments'].map(DomainFileAssignment_1.DomainFileAssignmentFromJSON)), @@ -8055,6 +8936,8 @@ function DomainClassificationResponseToJSON(value) { } return { 'runId': value['runId'], + 'graph': (0, DomainClassificationResponseGraph_1.DomainClassificationResponseGraphToJSON)(value['graph']), + 'metadata': (0, CodeGraphEnvelopeMetadata_1.CodeGraphEnvelopeMetadataToJSON)(value['metadata']), 'domains': (value['domains'].map(DomainSummary_1.DomainSummaryToJSON)), 'relationships': (value['relationships'].map(DomainRelationship_1.DomainRelationshipToJSON)), 'fileAssignments': (value['fileAssignments'].map(DomainFileAssignment_1.DomainFileAssignmentToJSON)), @@ -8067,6 +8950,141 @@ function DomainClassificationResponseToJSON(value) { } +/***/ }), + +/***/ 5501: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +/* tslint:disable */ +/* eslint-disable */ +/** + * Supermodel + * Code Graphing & Analysis API + * + * The version of the OpenAPI document: 0.9.3 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DomainClassificationResponseAsyncStatusEnum = void 0; +exports.instanceOfDomainClassificationResponseAsync = instanceOfDomainClassificationResponseAsync; +exports.DomainClassificationResponseAsyncFromJSON = DomainClassificationResponseAsyncFromJSON; +exports.DomainClassificationResponseAsyncFromJSONTyped = DomainClassificationResponseAsyncFromJSONTyped; +exports.DomainClassificationResponseAsyncToJSON = DomainClassificationResponseAsyncToJSON; +const DomainClassificationResponse_1 = __nccwpck_require__(9137); +/** + * @export + */ +exports.DomainClassificationResponseAsyncStatusEnum = { + Pending: 'pending', + Processing: 'processing', + Completed: 'completed', + Failed: 'failed' +}; +/** + * Check if a given object implements the DomainClassificationResponseAsync interface. + */ +function instanceOfDomainClassificationResponseAsync(value) { + if (!('status' in value) || value['status'] === undefined) + return false; + if (!('jobId' in value) || value['jobId'] === undefined) + return false; + return true; +} +function DomainClassificationResponseAsyncFromJSON(json) { + return DomainClassificationResponseAsyncFromJSONTyped(json, false); +} +function DomainClassificationResponseAsyncFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'status': json['status'], + 'jobId': json['jobId'], + 'retryAfter': json['retryAfter'] == null ? undefined : json['retryAfter'], + 'error': json['error'] == null ? undefined : json['error'], + 'result': json['result'] == null ? undefined : (0, DomainClassificationResponse_1.DomainClassificationResponseFromJSON)(json['result']), + }; +} +function DomainClassificationResponseAsyncToJSON(value) { + if (value == null) { + return value; + } + return { + 'status': value['status'], + 'jobId': value['jobId'], + 'retryAfter': value['retryAfter'], + 'error': value['error'], + 'result': (0, DomainClassificationResponse_1.DomainClassificationResponseToJSON)(value['result']), + }; +} + + +/***/ }), + +/***/ 9625: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +/* tslint:disable */ +/* eslint-disable */ +/** + * Supermodel + * Code Graphing & Analysis API + * + * The version of the OpenAPI document: 0.9.3 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.instanceOfDomainClassificationResponseGraph = instanceOfDomainClassificationResponseGraph; +exports.DomainClassificationResponseGraphFromJSON = DomainClassificationResponseGraphFromJSON; +exports.DomainClassificationResponseGraphFromJSONTyped = DomainClassificationResponseGraphFromJSONTyped; +exports.DomainClassificationResponseGraphToJSON = DomainClassificationResponseGraphToJSON; +const CodeGraphNode_1 = __nccwpck_require__(1811); +const CodeGraphRelationship_1 = __nccwpck_require__(5473); +/** + * Check if a given object implements the DomainClassificationResponseGraph interface. + */ +function instanceOfDomainClassificationResponseGraph(value) { + if (!('nodes' in value) || value['nodes'] === undefined) + return false; + if (!('relationships' in value) || value['relationships'] === undefined) + return false; + return true; +} +function DomainClassificationResponseGraphFromJSON(json) { + return DomainClassificationResponseGraphFromJSONTyped(json, false); +} +function DomainClassificationResponseGraphFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'nodes': (json['nodes'].map(CodeGraphNode_1.CodeGraphNodeFromJSON)), + 'relationships': (json['relationships'].map(CodeGraphRelationship_1.CodeGraphRelationshipFromJSON)), + }; +} +function DomainClassificationResponseGraphToJSON(value) { + if (value == null) { + return value; + } + return { + 'nodes': (value['nodes'].map(CodeGraphNode_1.CodeGraphNodeToJSON)), + 'relationships': (value['relationships'].map(CodeGraphRelationship_1.CodeGraphRelationshipToJSON)), + }; +} + + /***/ }), /***/ 385: @@ -8080,7 +9098,7 @@ function DomainClassificationResponseToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -8138,7 +9156,7 @@ function DomainFileAssignmentToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -8198,7 +9216,7 @@ function DomainFunctionAssignmentToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -8266,7 +9284,7 @@ function DomainRelationshipToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -8324,6 +9342,89 @@ function DomainSummaryToJSON(value) { } +/***/ }), + +/***/ 8512: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +/* tslint:disable */ +/* eslint-disable */ +/** + * Supermodel + * Code Graphing & Analysis API + * + * The version of the OpenAPI document: 0.9.3 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.EntryPointTypeEnum = void 0; +exports.instanceOfEntryPoint = instanceOfEntryPoint; +exports.EntryPointFromJSON = EntryPointFromJSON; +exports.EntryPointFromJSONTyped = EntryPointFromJSONTyped; +exports.EntryPointToJSON = EntryPointToJSON; +/** + * @export + */ +exports.EntryPointTypeEnum = { + Function: 'function', + Class: 'class', + Method: 'method', + Interface: 'interface', + Type: 'type', + Variable: 'variable', + Constant: 'constant' +}; +/** + * Check if a given object implements the EntryPoint interface. + */ +function instanceOfEntryPoint(value) { + if (!('file' in value) || value['file'] === undefined) + return false; + if (!('name' in value) || value['name'] === undefined) + return false; + if (!('line' in value) || value['line'] === undefined) + return false; + if (!('type' in value) || value['type'] === undefined) + return false; + if (!('reason' in value) || value['reason'] === undefined) + return false; + return true; +} +function EntryPointFromJSON(json) { + return EntryPointFromJSONTyped(json, false); +} +function EntryPointFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'file': json['file'], + 'name': json['name'], + 'line': json['line'], + 'type': json['type'], + 'reason': json['reason'], + }; +} +function EntryPointToJSON(value) { + if (value == null) { + return value; + } + return { + 'file': value['file'], + 'name': value['name'], + 'line': value['line'], + 'type': value['type'], + 'reason': value['reason'], + }; +} + + /***/ }), /***/ 8560: @@ -8337,7 +9438,7 @@ function DomainSummaryToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -8397,7 +9498,7 @@ function ErrorDetailsInnerToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -8444,6 +9545,78 @@ function FunctionDescriptionToJSON(value) { } +/***/ }), + +/***/ 2185: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +/* tslint:disable */ +/* eslint-disable */ +/** + * Supermodel + * Code Graphing & Analysis API + * + * The version of the OpenAPI document: 0.9.3 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.JobStatusStatusEnum = void 0; +exports.instanceOfJobStatus = instanceOfJobStatus; +exports.JobStatusFromJSON = JobStatusFromJSON; +exports.JobStatusFromJSONTyped = JobStatusFromJSONTyped; +exports.JobStatusToJSON = JobStatusToJSON; +/** + * @export + */ +exports.JobStatusStatusEnum = { + Pending: 'pending', + Processing: 'processing', + Completed: 'completed', + Failed: 'failed' +}; +/** + * Check if a given object implements the JobStatus interface. + */ +function instanceOfJobStatus(value) { + if (!('status' in value) || value['status'] === undefined) + return false; + if (!('jobId' in value) || value['jobId'] === undefined) + return false; + return true; +} +function JobStatusFromJSON(json) { + return JobStatusFromJSONTyped(json, false); +} +function JobStatusFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'status': json['status'], + 'jobId': json['jobId'], + 'retryAfter': json['retryAfter'] == null ? undefined : json['retryAfter'], + 'error': json['error'] == null ? undefined : json['error'], + }; +} +function JobStatusToJSON(value) { + if (value == null) { + return value; + } + return { + 'status': value['status'], + 'jobId': value['jobId'], + 'retryAfter': value['retryAfter'], + 'error': value['error'], + }; +} + + /***/ }), /***/ 4203: @@ -8457,7 +9630,7 @@ function FunctionDescriptionToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -8528,7 +9701,7 @@ function ModelErrorToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -8548,6 +9721,12 @@ function instanceOfSubdomainSummary(value) { return false; if (!('descriptionSummary' in value) || value['descriptionSummary'] === undefined) return false; + if (!('files' in value) || value['files'] === undefined) + return false; + if (!('functions' in value) || value['functions'] === undefined) + return false; + if (!('classes' in value) || value['classes'] === undefined) + return false; return true; } function SubdomainSummaryFromJSON(json) { @@ -8560,6 +9739,9 @@ function SubdomainSummaryFromJSONTyped(json, ignoreDiscriminator) { return { 'name': json['name'], 'descriptionSummary': json['descriptionSummary'], + 'files': json['files'], + 'functions': json['functions'], + 'classes': json['classes'], }; } function SubdomainSummaryToJSON(value) { @@ -8569,6 +9751,9 @@ function SubdomainSummaryToJSON(value) { return { 'name': value['name'], 'descriptionSummary': value['descriptionSummary'], + 'files': value['files'], + 'functions': value['functions'], + 'classes': value['classes'], }; } @@ -8586,7 +9771,7 @@ function SubdomainSummaryToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -8650,7 +9835,7 @@ function SupermodelArtifactToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -8663,6 +9848,9 @@ exports.SupermodelIRFromJSON = SupermodelIRFromJSON; exports.SupermodelIRFromJSONTyped = SupermodelIRFromJSONTyped; exports.SupermodelIRToJSON = SupermodelIRToJSON; const SupermodelIRGraph_1 = __nccwpck_require__(1881); +const SupermodelIRStats_1 = __nccwpck_require__(7654); +const DomainSummary_1 = __nccwpck_require__(7228); +const CodeGraphEnvelopeMetadata_1 = __nccwpck_require__(7122); const SupermodelArtifact_1 = __nccwpck_require__(4020); /** * Check if a given object implements the SupermodelIR interface. @@ -8676,6 +9864,12 @@ function instanceOfSupermodelIR(value) { return false; if (!('generatedAt' in value) || value['generatedAt'] === undefined) return false; + if (!('stats' in value) || value['stats'] === undefined) + return false; + if (!('metadata' in value) || value['metadata'] === undefined) + return false; + if (!('domains' in value) || value['domains'] === undefined) + return false; if (!('graph' in value) || value['graph'] === undefined) return false; return true; @@ -8693,6 +9887,9 @@ function SupermodelIRFromJSONTyped(json, ignoreDiscriminator) { 'schemaVersion': json['schemaVersion'], 'generatedAt': (new Date(json['generatedAt'])), 'summary': json['summary'] == null ? undefined : json['summary'], + 'stats': (0, SupermodelIRStats_1.SupermodelIRStatsFromJSON)(json['stats']), + 'metadata': (0, CodeGraphEnvelopeMetadata_1.CodeGraphEnvelopeMetadataFromJSON)(json['metadata']), + 'domains': (json['domains'].map(DomainSummary_1.DomainSummaryFromJSON)), 'graph': (0, SupermodelIRGraph_1.SupermodelIRGraphFromJSON)(json['graph']), 'artifacts': json['artifacts'] == null ? undefined : (json['artifacts'].map(SupermodelArtifact_1.SupermodelArtifactFromJSON)), }; @@ -8707,12 +9904,90 @@ function SupermodelIRToJSON(value) { 'schemaVersion': value['schemaVersion'], 'generatedAt': ((value['generatedAt']).toISOString()), 'summary': value['summary'], + 'stats': (0, SupermodelIRStats_1.SupermodelIRStatsToJSON)(value['stats']), + 'metadata': (0, CodeGraphEnvelopeMetadata_1.CodeGraphEnvelopeMetadataToJSON)(value['metadata']), + 'domains': (value['domains'].map(DomainSummary_1.DomainSummaryToJSON)), 'graph': (0, SupermodelIRGraph_1.SupermodelIRGraphToJSON)(value['graph']), 'artifacts': value['artifacts'] == null ? undefined : (value['artifacts'].map(SupermodelArtifact_1.SupermodelArtifactToJSON)), }; } +/***/ }), + +/***/ 8397: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +/* tslint:disable */ +/* eslint-disable */ +/** + * Supermodel + * Code Graphing & Analysis API + * + * The version of the OpenAPI document: 0.9.3 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SupermodelIRAsyncStatusEnum = void 0; +exports.instanceOfSupermodelIRAsync = instanceOfSupermodelIRAsync; +exports.SupermodelIRAsyncFromJSON = SupermodelIRAsyncFromJSON; +exports.SupermodelIRAsyncFromJSONTyped = SupermodelIRAsyncFromJSONTyped; +exports.SupermodelIRAsyncToJSON = SupermodelIRAsyncToJSON; +const SupermodelIR_1 = __nccwpck_require__(3569); +/** + * @export + */ +exports.SupermodelIRAsyncStatusEnum = { + Pending: 'pending', + Processing: 'processing', + Completed: 'completed', + Failed: 'failed' +}; +/** + * Check if a given object implements the SupermodelIRAsync interface. + */ +function instanceOfSupermodelIRAsync(value) { + if (!('status' in value) || value['status'] === undefined) + return false; + if (!('jobId' in value) || value['jobId'] === undefined) + return false; + return true; +} +function SupermodelIRAsyncFromJSON(json) { + return SupermodelIRAsyncFromJSONTyped(json, false); +} +function SupermodelIRAsyncFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'status': json['status'], + 'jobId': json['jobId'], + 'retryAfter': json['retryAfter'] == null ? undefined : json['retryAfter'], + 'error': json['error'] == null ? undefined : json['error'], + 'result': json['result'] == null ? undefined : (0, SupermodelIR_1.SupermodelIRFromJSON)(json['result']), + }; +} +function SupermodelIRAsyncToJSON(value) { + if (value == null) { + return value; + } + return { + 'status': value['status'], + 'jobId': value['jobId'], + 'retryAfter': value['retryAfter'], + 'error': value['error'], + 'result': (0, SupermodelIR_1.SupermodelIRToJSON)(value['result']), + }; +} + + /***/ }), /***/ 1881: @@ -8726,7 +10001,7 @@ function SupermodelIRToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -8773,6 +10048,64 @@ function SupermodelIRGraphToJSON(value) { } +/***/ }), + +/***/ 7654: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +/* tslint:disable */ +/* eslint-disable */ +/** + * Supermodel + * Code Graphing & Analysis API + * + * The version of the OpenAPI document: 0.9.3 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.instanceOfSupermodelIRStats = instanceOfSupermodelIRStats; +exports.SupermodelIRStatsFromJSON = SupermodelIRStatsFromJSON; +exports.SupermodelIRStatsFromJSONTyped = SupermodelIRStatsFromJSONTyped; +exports.SupermodelIRStatsToJSON = SupermodelIRStatsToJSON; +/** + * Check if a given object implements the SupermodelIRStats interface. + */ +function instanceOfSupermodelIRStats(value) { + return true; +} +function SupermodelIRStatsFromJSON(json) { + return SupermodelIRStatsFromJSONTyped(json, false); +} +function SupermodelIRStatsFromJSONTyped(json, ignoreDiscriminator) { + if (json == null) { + return json; + } + return { + 'nodeCount': json['nodeCount'] == null ? undefined : json['nodeCount'], + 'relationshipCount': json['relationshipCount'] == null ? undefined : json['relationshipCount'], + 'nodeTypes': json['nodeTypes'] == null ? undefined : json['nodeTypes'], + 'relationshipTypes': json['relationshipTypes'] == null ? undefined : json['relationshipTypes'], + }; +} +function SupermodelIRStatsToJSON(value) { + if (value == null) { + return value; + } + return { + 'nodeCount': value['nodeCount'], + 'relationshipCount': value['relationshipCount'], + 'nodeTypes': value['nodeTypes'], + 'relationshipTypes': value['relationshipTypes'], + }; +} + + /***/ }), /***/ 129: @@ -8786,7 +10119,7 @@ function SupermodelIRGraphToJSON(value) { * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -8855,25 +10188,38 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) { Object.defineProperty(exports, "__esModule", ({ value: true })); /* tslint:disable */ /* eslint-disable */ +__exportStar(__nccwpck_require__(5331), exports); __exportStar(__nccwpck_require__(6367), exports); __exportStar(__nccwpck_require__(9995), exports); +__exportStar(__nccwpck_require__(8711), exports); __exportStar(__nccwpck_require__(727), exports); +__exportStar(__nccwpck_require__(7122), exports); __exportStar(__nccwpck_require__(1811), exports); __exportStar(__nccwpck_require__(5473), exports); __exportStar(__nccwpck_require__(2208), exports); +__exportStar(__nccwpck_require__(8386), exports); +__exportStar(__nccwpck_require__(536), exports); +__exportStar(__nccwpck_require__(3750), exports); +__exportStar(__nccwpck_require__(4196), exports); __exportStar(__nccwpck_require__(4181), exports); __exportStar(__nccwpck_require__(9137), exports); +__exportStar(__nccwpck_require__(5501), exports); +__exportStar(__nccwpck_require__(9625), exports); __exportStar(__nccwpck_require__(385), exports); __exportStar(__nccwpck_require__(5903), exports); __exportStar(__nccwpck_require__(1988), exports); __exportStar(__nccwpck_require__(7228), exports); +__exportStar(__nccwpck_require__(8512), exports); __exportStar(__nccwpck_require__(8560), exports); __exportStar(__nccwpck_require__(8942), exports); +__exportStar(__nccwpck_require__(2185), exports); __exportStar(__nccwpck_require__(4203), exports); __exportStar(__nccwpck_require__(8268), exports); __exportStar(__nccwpck_require__(4020), exports); __exportStar(__nccwpck_require__(3569), exports); +__exportStar(__nccwpck_require__(8397), exports); __exportStar(__nccwpck_require__(1881), exports); +__exportStar(__nccwpck_require__(7654), exports); __exportStar(__nccwpck_require__(129), exports); @@ -8890,7 +10236,7 @@ __exportStar(__nccwpck_require__(129), exports); * Supermodel * Code Graphing & Analysis API * - * The version of the OpenAPI document: 0.4.1 + * The version of the OpenAPI document: 0.9.3 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -32286,156 +33632,64 @@ function wrappy (fn, cb) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.ENTRY_POINT_FUNCTION_NAMES = exports.ENTRY_POINT_PATTERNS = exports.DEFAULT_EXCLUDE_PATTERNS = void 0; -exports.isEntryPointFile = isEntryPointFile; -exports.isEntryPointFunction = isEntryPointFunction; -exports.shouldIgnoreFile = shouldIgnoreFile; -exports.findDeadCode = findDeadCode; +exports.filterByIgnorePatterns = filterByIgnorePatterns; exports.formatPrComment = formatPrComment; const minimatch_1 = __nccwpck_require__(6507); -/** Default glob patterns for files to exclude from dead code analysis. */ -exports.DEFAULT_EXCLUDE_PATTERNS = [ - '**/node_modules/**', - '**/dist/**', - '**/build/**', - '**/.git/**', - '**/vendor/**', - '**/target/**', - '**/*.test.ts', - '**/*.test.tsx', - '**/*.test.js', - '**/*.test.jsx', - '**/*.spec.ts', - '**/*.spec.tsx', - '**/*.spec.js', - '**/*.spec.jsx', - '**/__tests__/**', - '**/__mocks__/**', -]; -/** Glob patterns for files that are considered entry points. */ -exports.ENTRY_POINT_PATTERNS = [ - '**/index.ts', - '**/index.js', - '**/main.ts', - '**/main.js', - '**/app.ts', - '**/app.js', - '**/*.test.*', - '**/*.spec.*', - '**/__tests__/**', -]; -/** Function names that are considered entry points. */ -exports.ENTRY_POINT_FUNCTION_NAMES = [ - 'main', - 'run', - 'start', - 'init', - 'setup', - 'bootstrap', - 'default', - 'handler', - 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', -]; -/** - * Checks if a file path matches any entry point pattern. - * @param filePath - The file path to check - * @returns True if the file is an entry point - */ -function isEntryPointFile(filePath) { - return exports.ENTRY_POINT_PATTERNS.some(pattern => (0, minimatch_1.minimatch)(filePath, pattern)); -} +const markdown_1 = __nccwpck_require__(3758); /** - * Checks if a function name is a common entry point name. - * @param name - The function name to check - * @returns True if the function name is an entry point + * Filters dead code candidates by user-provided ignore patterns. + * The API handles all analysis server-side; this is purely for + * client-side post-filtering on file paths. */ -function isEntryPointFunction(name) { - const lowerName = name.toLowerCase(); - return exports.ENTRY_POINT_FUNCTION_NAMES.some(ep => lowerName === ep.toLowerCase()); -} -/** - * Checks if a file should be ignored based on exclude patterns. - * @param filePath - The file path to check - * @param ignorePatterns - Additional patterns to ignore - * @returns True if the file should be ignored - */ -function shouldIgnoreFile(filePath, ignorePatterns = []) { - const allPatterns = [...exports.DEFAULT_EXCLUDE_PATTERNS, ...ignorePatterns]; - return allPatterns.some(pattern => (0, minimatch_1.minimatch)(filePath, pattern)); -} -/** - * Analyzes a code graph to find functions that are never called. - * @param nodes - All nodes from the code graph - * @param relationships - All relationships from the code graph - * @param ignorePatterns - Additional glob patterns to ignore - * @returns Array of potentially unused functions - */ -function findDeadCode(nodes, relationships, ignorePatterns = []) { - const functionNodes = nodes.filter(node => node.labels?.includes('Function')); - const callRelationships = relationships.filter(rel => rel.type === 'calls'); - const calledFunctionIds = new Set(callRelationships.map(rel => rel.endNode)); - const deadCode = []; - for (const node of functionNodes) { - const props = node.properties || {}; - const filePath = props.filePath || props.file || ''; - const name = props.name || 'anonymous'; - if (calledFunctionIds.has(node.id)) { - continue; - } - if (shouldIgnoreFile(filePath, ignorePatterns)) { - continue; - } - if (isEntryPointFile(filePath)) { - continue; - } - if (isEntryPointFunction(name)) { - continue; - } - if (props.exported === true || props.isExported === true) { - continue; - } - deadCode.push({ - id: node.id, - name, - filePath, - startLine: props.startLine, - endLine: props.endLine, - }); - } - return deadCode; +function filterByIgnorePatterns(candidates, ignorePatterns) { + if (ignorePatterns.length === 0) + return candidates; + return candidates.filter(c => !ignorePatterns.some(p => (0, minimatch_1.minimatch)(c.file, p))); } /** - * Formats dead code results as a GitHub PR comment. - * @param deadCode - Array of dead code results - * @returns Markdown-formatted comment string + * Formats dead code analysis results as a GitHub PR comment. */ -function formatPrComment(deadCode) { - if (deadCode.length === 0) { +function formatPrComment(candidates, metadata) { + if (candidates.length === 0) { return `## Dead Code Hunter No dead code found! Your codebase is clean.`; } - const rows = deadCode + const rows = candidates .slice(0, 50) .map(dc => { - const lineInfo = dc.startLine ? `L${dc.startLine}` : ''; - const fileLink = dc.startLine - ? `${dc.filePath}#L${dc.startLine}` - : dc.filePath; - return `| \`${dc.name}\` | ${fileLink} | ${lineInfo} |`; + const lineInfo = dc.line ? `L${dc.line}` : ''; + const fileLink = dc.line ? `${dc.file}#L${dc.line}` : dc.file; + const badge = dc.confidence === 'high' ? ':red_circle:' : + dc.confidence === 'medium' ? ':orange_circle:' : ':yellow_circle:'; + return `| \`${(0, markdown_1.escapeTableCell)(dc.name)}\` | ${dc.type} | ${fileLink} | ${lineInfo} | ${badge} ${dc.confidence} |`; }) .join('\n'); let comment = `## Dead Code Hunter -Found **${deadCode.length}** potentially unused function${deadCode.length === 1 ? '' : 's'}: +Found **${candidates.length}** potentially unused code element${candidates.length === 1 ? '' : 's'}: -| Function | File | Line | -|----------|------|------| +| Name | Type | File | Line | Confidence | +|------|------|------|------|------------| ${rows}`; - if (deadCode.length > 50) { - comment += `\n\n_...and ${deadCode.length - 50} more. See action output for full list._`; + if (candidates.length > 50) { + comment += `\n\n_...and ${candidates.length - 50} more. See action output for full list._`; + } + if (metadata) { + comment += `\n\n
Analysis summary\n\n`; + comment += `- **Total declarations analyzed**: ${metadata.totalDeclarations}\n`; + comment += `- **Dead code candidates**: ${metadata.deadCodeCandidates}\n`; + comment += `- **Alive code**: ${metadata.aliveCode}\n`; + comment += `- **Analysis method**: ${metadata.analysisMethod}\n`; + if (metadata.transitiveDeadCount != null) { + comment += `- **Transitive dead**: ${metadata.transitiveDeadCount}\n`; + } + if (metadata.symbolLevelDeadCount != null) { + comment += `- **Symbol-level dead**: ${metadata.symbolLevelDeadCount}\n`; + } + comment += `\n
`; } - comment += `\n\n---\n_Powered by [Supermodel](https://supermodeltools.com) graph analysis_`; + comment += `\n\n---\n_Powered by [Supermodel](https://supermodeltools.com) dead code analysis_`; return comment; } @@ -32504,6 +33758,9 @@ const SENSITIVE_KEYS = new Set([ 'x-api-key', ]); const MAX_VALUE_LENGTH = 1000; +const MAX_POLL_ATTEMPTS = 90; +const DEFAULT_RETRY_INTERVAL_MS = 10_000; +const POLL_TIMEOUT_MS = 15 * 60 * 1000; /** * Safely serialize a value for logging, handling circular refs, BigInt, and large values. * Redacts sensitive fields. @@ -32512,15 +33769,12 @@ function safeSerialize(value, maxLength = MAX_VALUE_LENGTH) { try { const seen = new WeakSet(); const serialized = JSON.stringify(value, (key, val) => { - // Redact sensitive keys if (key && SENSITIVE_KEYS.has(key.toLowerCase())) { return '[REDACTED]'; } - // Handle BigInt if (typeof val === 'bigint') { return val.toString(); } - // Handle circular references if (typeof val === 'object' && val !== null) { if (seen.has(val)) { return '[Circular]'; @@ -32529,7 +33783,6 @@ function safeSerialize(value, maxLength = MAX_VALUE_LENGTH) { } return val; }, 2); - // Truncate if too long if (serialized && serialized.length > maxLength) { return serialized.slice(0, maxLength) + '... [truncated]'; } @@ -32580,8 +33833,38 @@ async function generateIdempotencyKey(workspacePath) { }); const commitHash = output.trim(); const repoName = path.basename(workspacePath); - // Use UUID to ensure unique key per run (avoids 409 conflicts, scales to many concurrent users) - return `${repoName}:deadcode:${commitHash}:${(0, crypto_1.randomUUID)()}`; + return `${repoName}:analysis:deadcode:${commitHash}:${(0, crypto_1.randomUUID)()}`; +} +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} +/** + * Polls the dead code analysis endpoint until the job completes or fails. + * The API returns 202 while processing; re-submitting the same request + * with the same idempotency key acts as a poll. + */ +async function pollForResult(api, idempotencyKey, zipBlob) { + const startTime = Date.now(); + for (let attempt = 1; attempt <= MAX_POLL_ATTEMPTS; attempt++) { + const response = await api.generateDeadCodeAnalysis({ + idempotencyKey, + file: zipBlob, + }); + if (response.status === 'completed' && response.result) { + return response.result; + } + if (response.status === 'failed') { + throw new Error(`Analysis job failed: ${response.error || 'unknown error'}`); + } + const elapsed = Date.now() - startTime; + if (elapsed >= POLL_TIMEOUT_MS) { + throw new Error(`Analysis timed out after ${Math.round(elapsed / 1000)}s (job: ${response.jobId})`); + } + const retryMs = (response.retryAfter ?? DEFAULT_RETRY_INTERVAL_MS / 1000) * 1000; + core.info(`Job ${response.jobId} status: ${response.status} (attempt ${attempt}/${MAX_POLL_ATTEMPTS}, retry in ${retryMs / 1000}s)`); + await sleep(retryMs); + } + throw new Error(`Analysis did not complete within ${MAX_POLL_ATTEMPTS} polling attempts`); } async function run() { try { @@ -32598,7 +33881,7 @@ async function run() { const zipPath = await createZipArchive(workspacePath); // Step 2: Generate idempotency key const idempotencyKey = await generateIdempotencyKey(workspacePath); - // Step 3: Call Supermodel API + // Step 3: Call Supermodel dead code analysis API core.info('Analyzing codebase with Supermodel...'); const config = new sdk_1.Configuration({ basePath: process.env.SUPERMODEL_BASE_URL || 'https://api.supermodeltools.com', @@ -32607,24 +33890,24 @@ async function run() { const api = new sdk_1.DefaultApi(config); const zipBuffer = await fs.readFile(zipPath); const zipBlob = new Blob([zipBuffer], { type: 'application/zip' }); - const response = await api.generateSupermodelGraph({ - idempotencyKey, - file: zipBlob, - }); - // Step 4: Analyze for dead code - const nodes = response.graph?.nodes || []; - const relationships = response.graph?.relationships || []; - const deadCode = (0, dead_code_1.findDeadCode)(nodes, relationships, ignorePatterns); - core.info(`Found ${deadCode.length} potentially unused functions`); + const result = await pollForResult(api, idempotencyKey, zipBlob); + // Step 4: Apply client-side ignore patterns + const candidates = (0, dead_code_1.filterByIgnorePatterns)(result.deadCodeCandidates, ignorePatterns); + core.info(`Found ${candidates.length} potentially unused code elements (${result.metadata.totalDeclarations} declarations analyzed)`); + core.info(`Analysis method: ${result.metadata.analysisMethod}`); + core.info(`Alive: ${result.metadata.aliveCode}, Entry points: ${result.entryPoints.length}, Root files: ${result.metadata.rootFilesCount ?? 'n/a'}`); + for (const dc of candidates) { + core.info(` [${dc.confidence}] ${dc.type} ${dc.name} @ ${dc.file}:${dc.line} — ${dc.reason}`); + } // Step 5: Set outputs - core.setOutput('dead-code-count', deadCode.length); - core.setOutput('dead-code-json', JSON.stringify(deadCode)); + core.setOutput('dead-code-count', candidates.length); + core.setOutput('dead-code-json', JSON.stringify(candidates)); // Step 6: Post PR comment if enabled if (commentOnPr && github.context.payload.pull_request) { const token = core.getInput('github-token') || process.env.GITHUB_TOKEN; if (token) { const octokit = github.getOctokit(token); - const comment = (0, dead_code_1.formatPrComment)(deadCode); + const comment = (0, dead_code_1.formatPrComment)(candidates, result.metadata); await octokit.rest.issues.createComment({ owner: github.context.repo.owner, repo: github.context.repo.repo, @@ -32640,24 +33923,20 @@ async function run() { // Step 7: Clean up await fs.unlink(zipPath); // Step 8: Fail if configured and dead code found - if (deadCode.length > 0 && failOnDeadCode) { - core.setFailed(`Found ${deadCode.length} potentially unused functions`); + if (candidates.length > 0 && failOnDeadCode) { + core.setFailed(`Found ${candidates.length} potentially unused code elements`); } } catch (error) { - // Log error details for debugging (using debug level for potentially sensitive data) core.info('--- Error Debug Info ---'); core.info(`Error type: ${error?.constructor?.name ?? 'unknown'}`); core.info(`Error message: ${error?.message ?? 'no message'}`); core.info(`Error name: ${error?.name ?? 'no name'}`); - // Check various error structures used by different HTTP clients - // Use core.debug for detailed/sensitive info, core.info for safe summaries try { if (error?.response) { core.info(`Response status: ${error.response.status ?? 'unknown'}`); core.info(`Response statusText: ${error.response.statusText ?? 'unknown'}`); core.info(`Response data: ${safeSerialize(error.response.data)}`); - // Headers may contain sensitive values - use debug level core.debug(`Response headers: ${safeSerialize(redactSensitive(error.response.headers))}`); } if (error?.body) { @@ -32679,10 +33958,8 @@ async function run() { core.info('--- End Debug Info ---'); let errorMessage = 'An unknown error occurred'; let helpText = ''; - // Try multiple error structures const status = error?.response?.status || error?.status || error?.statusCode; let apiMessage = ''; - // Try to extract message from various locations try { apiMessage = error?.response?.data?.message || @@ -32705,7 +33982,6 @@ async function run() { } else if (status === 500) { errorMessage = apiMessage || 'Internal server error'; - // Check for common issues and provide guidance if (apiMessage.includes('Nested archives')) { helpText = 'Your repository contains nested archive files (.zip, .tar, etc.). ' + 'Add them to .gitattributes with "export-ignore" to exclude from analysis. ' + @@ -32740,6 +34016,23 @@ async function run() { run(); +/***/ }), + +/***/ 3758: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.escapeTableCell = escapeTableCell; +/** + * Escapes pipe characters for safe rendering inside markdown tables. + */ +function escapeTableCell(text) { + return text.replace(/\|/g, '\\|').replace(/\n/g, ' '); +} + + /***/ }), /***/ 2613: diff --git a/dist/markdown.d.ts b/dist/markdown.d.ts new file mode 100644 index 0000000..f5d883b --- /dev/null +++ b/dist/markdown.d.ts @@ -0,0 +1,4 @@ +/** + * Escapes pipe characters for safe rendering inside markdown tables. + */ +export declare function escapeTableCell(text: string): string; diff --git a/dist/markdown.d.ts.map b/dist/markdown.d.ts.map new file mode 100644 index 0000000..a0760d3 --- /dev/null +++ b/dist/markdown.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"","sourceRoot":"","sources":["file:///Users/jag/repos/dead-code-hunter-issue-8/src/markdown.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD"} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ee9ba94..acc57df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@actions/core": "^1.10.1", "@actions/exec": "^1.1.1", "@actions/github": "^6.0.0", - "@supermodeltools/sdk": "^0.4.1", + "@supermodeltools/sdk": "^0.9.3", "minimatch": "^9.0.0" }, "devDependencies": { @@ -1047,9 +1047,9 @@ "license": "MIT" }, "node_modules/@supermodeltools/sdk": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@supermodeltools/sdk/-/sdk-0.4.1.tgz", - "integrity": "sha512-/hVzGvceyrv9S+HxaYhbp1DaX2B94t1td2kG2HiM5Ey4p59F5FojK7vOAurBcmCQ5Hhphp+BhgUWrgwd/sOSOQ==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@supermodeltools/sdk/-/sdk-0.9.3.tgz", + "integrity": "sha512-IDvNoke9ymCAnzHxJjMEj+USyuN53Dyulex/gpQ/sUBrvIM3PKaiVYxpr3oV5z+rhjnix1GqJHtXOw9yRYSw0w==", "license": "UNLICENSED" }, "node_modules/@types/chai": { diff --git a/package.json b/package.json index f283d56..ca21975 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dead-code-hunter", - "version": "0.1.0", - "description": "GitHub Action to find unreachable functions using Supermodel call graphs", + "version": "0.2.0", + "description": "GitHub Action to find dead code using the Supermodel dead code analysis API", "main": "dist/index.js", "scripts": { "build": "ncc build src/index.ts -o dist", @@ -25,7 +25,7 @@ "@actions/core": "^1.10.1", "@actions/exec": "^1.1.1", "@actions/github": "^6.0.0", - "@supermodeltools/sdk": "^0.4.1", + "@supermodeltools/sdk": "^0.9.3", "minimatch": "^9.0.0" }, "devDependencies": { diff --git a/src/__tests__/dead-code.test.ts b/src/__tests__/dead-code.test.ts index 5a8c6d3..c966278 100644 --- a/src/__tests__/dead-code.test.ts +++ b/src/__tests__/dead-code.test.ts @@ -1,261 +1,203 @@ import { describe, it, expect } from 'vitest'; -import { - findDeadCode, - isEntryPointFile, - isEntryPointFunction, - shouldIgnoreFile, - formatPrComment, - DeadCodeResult, -} from '../dead-code'; -import { CodeGraphNode, CodeGraphRelationship } from '@supermodeltools/sdk'; +import { filterByIgnorePatterns, formatPrComment } from '../dead-code'; +import { escapeTableCell } from '../markdown'; +import type { DeadCodeCandidate, DeadCodeAnalysisMetadata } from '@supermodeltools/sdk'; -describe('isEntryPointFile', () => { - it('should identify index files as entry points', () => { - expect(isEntryPointFile('src/index.ts')).toBe(true); - expect(isEntryPointFile('lib/index.js')).toBe(true); - }); - - it('should identify main files as entry points', () => { - expect(isEntryPointFile('src/main.ts')).toBe(true); - expect(isEntryPointFile('main.js')).toBe(true); - }); - - it('should identify app files as entry points', () => { - expect(isEntryPointFile('src/app.ts')).toBe(true); - }); +function makeCandidate(overrides: Partial = {}): DeadCodeCandidate { + return { + file: 'src/utils.ts', + name: 'unusedFn', + line: 10, + type: 'function' as const, + confidence: 'high' as const, + reason: 'No callers found in codebase', + ...overrides, + }; +} - it('should identify test files as entry points', () => { - expect(isEntryPointFile('src/utils.test.ts')).toBe(true); - expect(isEntryPointFile('src/utils.spec.js')).toBe(true); - expect(isEntryPointFile('src/__tests__/utils.ts')).toBe(true); - }); - - it('should not identify regular files as entry points', () => { - expect(isEntryPointFile('src/utils.ts')).toBe(false); - expect(isEntryPointFile('src/helpers/format.js')).toBe(false); - }); -}); +function makeMetadata(overrides: Partial = {}): DeadCodeAnalysisMetadata { + return { + totalDeclarations: 100, + deadCodeCandidates: 5, + aliveCode: 95, + analysisMethod: 'parse_graph + call_graph', + ...overrides, + }; +} -describe('isEntryPointFunction', () => { - it('should identify common entry point function names', () => { - expect(isEntryPointFunction('main')).toBe(true); - expect(isEntryPointFunction('run')).toBe(true); - expect(isEntryPointFunction('start')).toBe(true); - expect(isEntryPointFunction('init')).toBe(true); - expect(isEntryPointFunction('handler')).toBe(true); +describe('escapeTableCell', () => { + it('should escape pipe characters', () => { + expect(escapeTableCell('a|b|c')).toBe('a\\|b\\|c'); }); - it('should be case-insensitive', () => { - expect(isEntryPointFunction('Main')).toBe(true); - expect(isEntryPointFunction('MAIN')).toBe(true); - expect(isEntryPointFunction('Handler')).toBe(true); + it('should replace newlines with spaces', () => { + expect(escapeTableCell('line1\nline2')).toBe('line1 line2'); }); - it('should identify HTTP method handlers', () => { - expect(isEntryPointFunction('GET')).toBe(true); - expect(isEntryPointFunction('POST')).toBe(true); - expect(isEntryPointFunction('PUT')).toBe(true); - expect(isEntryPointFunction('DELETE')).toBe(true); + it('should handle both pipes and newlines', () => { + expect(escapeTableCell('a|b\nc|d')).toBe('a\\|b c\\|d'); }); - it('should not identify regular function names', () => { - expect(isEntryPointFunction('processData')).toBe(false); - expect(isEntryPointFunction('calculateTotal')).toBe(false); + it('should return unchanged string when no special characters', () => { + expect(escapeTableCell('normalText')).toBe('normalText'); }); }); -describe('shouldIgnoreFile', () => { - it('should ignore node_modules', () => { - expect(shouldIgnoreFile('node_modules/lodash/index.js')).toBe(true); +describe('filterByIgnorePatterns', () => { + it('should return all candidates when no patterns provided', () => { + const candidates = [makeCandidate(), makeCandidate({ file: 'src/helpers.ts' })]; + const result = filterByIgnorePatterns(candidates, []); + expect(result).toHaveLength(2); }); - it('should ignore dist folder', () => { - expect(shouldIgnoreFile('dist/index.js')).toBe(true); - }); - - it('should ignore build folder', () => { - expect(shouldIgnoreFile('build/main.js')).toBe(true); + it('should filter candidates matching ignore patterns', () => { + const candidates = [ + makeCandidate({ file: 'src/generated/api.ts' }), + makeCandidate({ file: 'src/utils.ts' }), + ]; + const result = filterByIgnorePatterns(candidates, ['**/generated/**']); + expect(result).toHaveLength(1); + expect(result[0].file).toBe('src/utils.ts'); }); - it('should ignore test files', () => { - expect(shouldIgnoreFile('src/utils.test.ts')).toBe(true); - expect(shouldIgnoreFile('src/utils.spec.js')).toBe(true); + it('should support multiple ignore patterns', () => { + const candidates = [ + makeCandidate({ file: 'src/generated/api.ts' }), + makeCandidate({ file: 'src/migrations/001.ts' }), + makeCandidate({ file: 'src/utils.ts' }), + ]; + const result = filterByIgnorePatterns(candidates, ['**/generated/**', '**/migrations/**']); + expect(result).toHaveLength(1); + expect(result[0].file).toBe('src/utils.ts'); }); - it('should not ignore regular source files', () => { - expect(shouldIgnoreFile('src/utils.ts')).toBe(false); - expect(shouldIgnoreFile('lib/helpers.js')).toBe(false); + it('should not filter when patterns do not match', () => { + const candidates = [makeCandidate({ file: 'src/utils.ts' })]; + const result = filterByIgnorePatterns(candidates, ['**/generated/**']); + expect(result).toHaveLength(1); }); - it('should respect custom ignore patterns', () => { - expect(shouldIgnoreFile('src/generated/api.ts', ['**/generated/**'])).toBe(true); - expect(shouldIgnoreFile('src/utils.ts', ['**/generated/**'])).toBe(false); + it('should filter across different code types', () => { + const candidates = [ + makeCandidate({ file: 'src/generated/types.ts', type: 'interface' }), + makeCandidate({ file: 'src/generated/client.ts', type: 'class' }), + makeCandidate({ file: 'src/service.ts', type: 'function' }), + ]; + const result = filterByIgnorePatterns(candidates, ['**/generated/**']); + expect(result).toHaveLength(1); + expect(result[0].file).toBe('src/service.ts'); }); }); -describe('findDeadCode', () => { - it('should find functions with no callers', () => { - const nodes: CodeGraphNode[] = [ - { id: 'fn1', labels: ['Function'], properties: { name: 'usedFunction', filePath: 'src/utils.ts' } }, - { id: 'fn2', labels: ['Function'], properties: { name: 'unusedFunction', filePath: 'src/helpers.ts' } }, - ]; - - const relationships: CodeGraphRelationship[] = [ - { id: 'rel1', type: 'calls', startNode: 'fn3', endNode: 'fn1' }, - ]; - - const deadCode = findDeadCode(nodes, relationships); - - expect(deadCode).toHaveLength(1); - expect(deadCode[0].name).toBe('unusedFunction'); +describe('formatPrComment', () => { + it('should format empty results', () => { + const comment = formatPrComment([]); + expect(comment).toContain('No dead code found'); + expect(comment).toContain('codebase is clean'); }); - it('should not report functions that are called', () => { - const nodes: CodeGraphNode[] = [ - { id: 'fn1', labels: ['Function'], properties: { name: 'calledFunction', filePath: 'src/utils.ts' } }, - ]; - - const relationships: CodeGraphRelationship[] = [ - { id: 'rel1', type: 'calls', startNode: 'fn2', endNode: 'fn1' }, - ]; - - const deadCode = findDeadCode(nodes, relationships); + it('should format single result with type and confidence', () => { + const candidates = [makeCandidate()]; + const comment = formatPrComment(candidates); - expect(deadCode).toHaveLength(0); + expect(comment).toContain('1** potentially unused code element:'); + expect(comment).toContain('`unusedFn`'); + expect(comment).toContain('function'); + expect(comment).toContain('src/utils.ts#L10'); + expect(comment).toContain('high'); }); - it('should skip exported functions', () => { - const nodes: CodeGraphNode[] = [ - { id: 'fn1', labels: ['Function'], properties: { name: 'exportedFn', filePath: 'src/utils.ts', exported: true } }, - { id: 'fn2', labels: ['Function'], properties: { name: 'notExported', filePath: 'src/helpers.ts' } }, + it('should format multiple results', () => { + const candidates = [ + makeCandidate({ name: 'fn1', file: 'src/a.ts', line: 1 }), + makeCandidate({ name: 'fn2', file: 'src/b.ts', line: 2, type: 'class' as const }), ]; + const comment = formatPrComment(candidates); - const relationships: CodeGraphRelationship[] = []; - - const deadCode = findDeadCode(nodes, relationships); - - expect(deadCode).toHaveLength(1); - expect(deadCode[0].name).toBe('notExported'); + expect(comment).toContain('2** potentially unused code elements'); + expect(comment).toContain('`fn1`'); + expect(comment).toContain('`fn2`'); + expect(comment).toContain('class'); }); - it('should skip entry point functions', () => { - const nodes: CodeGraphNode[] = [ - { id: 'fn1', labels: ['Function'], properties: { name: 'main', filePath: 'src/cli.ts' } }, - { id: 'fn2', labels: ['Function'], properties: { name: 'unusedHelper', filePath: 'src/helpers.ts' } }, - ]; - - const relationships: CodeGraphRelationship[] = []; - - const deadCode = findDeadCode(nodes, relationships); + it('should render all supported code types', () => { + const types = ['function', 'class', 'method', 'interface', 'type', 'variable', 'constant'] as const; + const candidates = types.map((type, i) => + makeCandidate({ name: `item${i}`, file: `src/${type}.ts`, line: i + 1, type }) + ); + const comment = formatPrComment(candidates); - expect(deadCode).toHaveLength(1); - expect(deadCode[0].name).toBe('unusedHelper'); + for (const type of types) { + expect(comment).toContain(`| ${type} |`); + } }); - it('should skip functions in entry point files', () => { - const nodes: CodeGraphNode[] = [ - { id: 'fn1', labels: ['Function'], properties: { name: 'someFunc', filePath: 'src/index.ts' } }, - { id: 'fn2', labels: ['Function'], properties: { name: 'unusedHelper', filePath: 'src/helpers.ts' } }, - ]; - - const relationships: CodeGraphRelationship[] = []; - - const deadCode = findDeadCode(nodes, relationships); + it('should escape pipe characters in candidate names', () => { + const candidates = [makeCandidate({ name: 'fn|with|pipes' })]; + const comment = formatPrComment(candidates); - expect(deadCode).toHaveLength(1); - expect(deadCode[0].name).toBe('unusedHelper'); + expect(comment).toContain('fn\\|with\\|pipes'); + expect(comment).not.toContain('`fn|with|pipes`'); }); - it('should skip functions in ignored paths', () => { - const nodes: CodeGraphNode[] = [ - { id: 'fn1', labels: ['Function'], properties: { name: 'testHelper', filePath: 'src/__tests__/helpers.ts' } }, - { id: 'fn2', labels: ['Function'], properties: { name: 'unusedHelper', filePath: 'src/helpers.ts' } }, - ]; - - const relationships: CodeGraphRelationship[] = []; - - const deadCode = findDeadCode(nodes, relationships); + it('should truncate at 50 results', () => { + const candidates = Array.from({ length: 60 }, (_, i) => + makeCandidate({ name: `fn${i}`, file: `src/file${i}.ts`, line: i + 1 }) + ); + const comment = formatPrComment(candidates); - expect(deadCode).toHaveLength(1); - expect(deadCode[0].name).toBe('unusedHelper'); + expect(comment).toContain('60** potentially unused code elements'); + expect(comment).toContain('and 10 more'); }); - it('should only consider Function nodes', () => { - const nodes: CodeGraphNode[] = [ - { id: 'file1', labels: ['File'], properties: { name: 'utils.ts', filePath: 'src/utils.ts' } }, - { id: 'fn1', labels: ['Function'], properties: { name: 'unusedFn', filePath: 'src/helpers.ts' } }, - ]; - - const relationships: CodeGraphRelationship[] = []; - - const deadCode = findDeadCode(nodes, relationships); + it('should include metadata details section when provided', () => { + const candidates = [makeCandidate()]; + const metadata = makeMetadata({ transitiveDeadCount: 3, symbolLevelDeadCount: 7 }); + const comment = formatPrComment(candidates, metadata); - expect(deadCode).toHaveLength(1); - expect(deadCode[0].name).toBe('unusedFn'); + expect(comment).toContain('Analysis summary'); + expect(comment).toContain('Total declarations analyzed'); + expect(comment).toContain('100'); + expect(comment).toContain('parse_graph + call_graph'); + expect(comment).toContain('Transitive dead'); + expect(comment).toContain('3'); + expect(comment).toContain('Symbol-level dead'); + expect(comment).toContain('7'); }); - it('should include line numbers when available', () => { - const nodes: CodeGraphNode[] = [ - { id: 'fn1', labels: ['Function'], properties: { name: 'unusedFn', filePath: 'src/helpers.ts', startLine: 10, endLine: 20 } }, - ]; + it('should omit optional metadata fields when not present', () => { + const candidates = [makeCandidate()]; + const metadata = makeMetadata(); + const comment = formatPrComment(candidates, metadata); - const deadCode = findDeadCode(nodes, []); - - expect(deadCode[0].startLine).toBe(10); - expect(deadCode[0].endLine).toBe(20); - }); -}); - -describe('formatPrComment', () => { - it('should format empty results', () => { - const comment = formatPrComment([]); - expect(comment).toContain('No dead code found'); - expect(comment).toContain('codebase is clean'); + expect(comment).toContain('Analysis summary'); + expect(comment).not.toContain('Transitive dead'); + expect(comment).not.toContain('Symbol-level dead'); }); - it('should format single result', () => { - const deadCode: DeadCodeResult[] = [ - { id: 'fn1', name: 'unusedFn', filePath: 'src/utils.ts', startLine: 10 }, + it('should show confidence badges', () => { + const candidates = [ + makeCandidate({ confidence: 'high' as const }), + makeCandidate({ name: 'fn2', file: 'src/b.ts', confidence: 'medium' as const }), + makeCandidate({ name: 'fn3', file: 'src/c.ts', confidence: 'low' as const }), ]; + const comment = formatPrComment(candidates); - const comment = formatPrComment(deadCode); - - expect(comment).toContain('1** potentially unused function'); - expect(comment).toContain('`unusedFn`'); - expect(comment).toContain('src/utils.ts#L10'); + expect(comment).toContain(':red_circle: high'); + expect(comment).toContain(':orange_circle: medium'); + expect(comment).toContain(':yellow_circle: low'); }); - it('should format multiple results', () => { - const deadCode: DeadCodeResult[] = [ - { id: 'fn1', name: 'unusedFn1', filePath: 'src/utils.ts', startLine: 10 }, - { id: 'fn2', name: 'unusedFn2', filePath: 'src/helpers.ts', startLine: 20 }, - ]; - - const comment = formatPrComment(deadCode); - - expect(comment).toContain('2** potentially unused functions'); - expect(comment).toContain('`unusedFn1`'); - expect(comment).toContain('`unusedFn2`'); + it('should include Supermodel attribution', () => { + const candidates = [makeCandidate()]; + const comment = formatPrComment(candidates); + expect(comment).toContain('Powered by [Supermodel]'); }); - it('should truncate at 50 results', () => { - const deadCode: DeadCodeResult[] = Array.from({ length: 60 }, (_, i) => ({ - id: `fn${i}`, - name: `unusedFn${i}`, - filePath: `src/file${i}.ts`, - })); + it('should render table header with all columns', () => { + const candidates = [makeCandidate()]; + const comment = formatPrComment(candidates); - const comment = formatPrComment(deadCode); - - expect(comment).toContain('60** potentially unused functions'); - expect(comment).toContain('and 10 more'); - }); - - it('should include Supermodel attribution when dead code found', () => { - const deadCode: DeadCodeResult[] = [ - { id: 'fn1', name: 'unusedFn', filePath: 'src/utils.ts' }, - ]; - const comment = formatPrComment(deadCode); - expect(comment).toContain('Powered by [Supermodel]'); + expect(comment).toContain('| Name | Type | File | Line | Confidence |'); }); }); diff --git a/src/__tests__/integration.test.ts b/src/__tests__/integration.test.ts index 11dc794..67951f0 100644 --- a/src/__tests__/integration.test.ts +++ b/src/__tests__/integration.test.ts @@ -3,15 +3,50 @@ import { execSync } from 'child_process'; import * as fs from 'fs/promises'; import * as path from 'path'; import { Configuration, DefaultApi } from '@supermodeltools/sdk'; -import { findDeadCode } from '../dead-code'; +import type { DeadCodeAnalysisResponseAsync, DeadCodeAnalysisResponse } from '@supermodeltools/sdk'; +import { filterByIgnorePatterns } from '../dead-code'; const API_KEY = process.env.SUPERMODEL_API_KEY; const SKIP_INTEGRATION = !API_KEY; +async function pollForResult( + api: DefaultApi, + idempotencyKey: string, + zipBlob: Blob, + timeoutMs = 120_000 +): Promise { + const startTime = Date.now(); + + for (let attempt = 1; attempt <= 30; attempt++) { + const response: DeadCodeAnalysisResponseAsync = await api.generateDeadCodeAnalysis({ + idempotencyKey, + file: zipBlob, + }); + + if (response.status === 'completed' && response.result) { + return response.result; + } + + if (response.status === 'failed') { + throw new Error(`Analysis job failed: ${response.error || 'unknown error'}`); + } + + if (Date.now() - startTime >= timeoutMs) { + throw new Error(`Polling timed out after ${timeoutMs}ms`); + } + + const retryMs = (response.retryAfter ?? 10) * 1000; + await new Promise(resolve => setTimeout(resolve, retryMs)); + } + + throw new Error('Max polling attempts exceeded'); +} + describe.skipIf(SKIP_INTEGRATION)('Integration Tests', () => { let api: DefaultApi; let zipPath: string; let idempotencyKey: string; + let result: DeadCodeAnalysisResponse; beforeAll(async () => { const config = new Configuration({ @@ -20,7 +55,6 @@ describe.skipIf(SKIP_INTEGRATION)('Integration Tests', () => { }); api = new DefaultApi(config); - // Create zip of this repo (dead-code-hunter testing itself!) const repoRoot = path.resolve(__dirname, '../..'); zipPath = '/tmp/dead-code-hunter-test.zip'; @@ -29,68 +63,68 @@ describe.skipIf(SKIP_INTEGRATION)('Integration Tests', () => { const commitHash = execSync('git rev-parse --short HEAD', { cwd: repoRoot }) .toString() .trim(); - idempotencyKey = `dead-code-hunter:call:${commitHash}`; - }); + idempotencyKey = `dead-code-hunter:integration:${commitHash}`; - it('should call the Supermodel API and get a call graph', async () => { const zipBuffer = await fs.readFile(zipPath); const zipBlob = new Blob([zipBuffer], { type: 'application/zip' }); + result = await pollForResult(api, idempotencyKey, zipBlob); + }, 120_000); + + it('should return a valid response shape', () => { + expect(result).toBeDefined(); + expect(result.metadata).toBeDefined(); + expect(result.deadCodeCandidates).toBeDefined(); + expect(result.aliveCode).toBeDefined(); + expect(result.entryPoints).toBeDefined(); + expect(result.metadata.totalDeclarations).toBeGreaterThan(0); + expect(typeof result.metadata.analysisMethod).toBe('string'); + }); - const response = await api.generateCallGraph({ - idempotencyKey, - file: zipBlob, - }); - - expect(response).toBeDefined(); - expect(response.graph).toBeDefined(); - expect(response.graph?.nodes).toBeDefined(); - expect(response.graph?.relationships).toBeDefined(); - expect(response.stats).toBeDefined(); - - console.log('API Stats:', response.stats); - console.log('Nodes:', response.graph?.nodes?.length); - console.log('Relationships:', response.graph?.relationships?.length); - }, 60000); // 60 second timeout for API call - - it('should find dead code in the dead-code-hunter repo itself', async () => { - const zipBuffer = await fs.readFile(zipPath); - const zipBlob = new Blob([zipBuffer], { type: 'application/zip' }); - - const response = await api.generateCallGraph({ - idempotencyKey, - file: zipBlob, - }); - - const nodes = response.graph?.nodes || []; - const relationships = response.graph?.relationships || []; + it('should analyze the codebase without errors', () => { + console.log('\n=== Dead Code Analysis Results ==='); + console.log(`Total declarations: ${result.metadata.totalDeclarations}`); + console.log(`Dead code candidates: ${result.deadCodeCandidates.length}`); + console.log(`Alive code: ${result.metadata.aliveCode}`); + console.log(`Analysis method: ${result.metadata.analysisMethod}`); - const deadCode = findDeadCode(nodes, relationships); + for (const dc of result.deadCodeCandidates) { + console.log(` [${dc.confidence}] ${dc.type} ${dc.name} @ ${dc.file}:${dc.line} — ${dc.reason}`); + } - console.log('\n=== Dead Code Hunter Self-Analysis ==='); - console.log(`Total functions: ${nodes.filter(n => n.labels?.includes('Function')).length}`); - console.log(`Total call relationships: ${relationships.filter(r => r.type === 'calls').length}`); - console.log(`Dead code found: ${deadCode.length}`); + expect(result.metadata.totalDeclarations).toBeGreaterThan(0); + }); - if (deadCode.length > 0) { - console.log('\nPotentially dead functions:'); - for (const dc of deadCode.slice(0, 10)) { - console.log(` - ${dc.name} (${dc.filePath}:${dc.startLine || '?'})`); - } + it('should include valid fields on every candidate', () => { + for (const dc of result.deadCodeCandidates) { + expect(dc.file).toBeTruthy(); + expect(dc.name).toBeTruthy(); + expect(dc.line).toBeGreaterThan(0); + expect(dc.type).toBeTruthy(); + expect(['high', 'medium', 'low']).toContain(dc.confidence); + expect(dc.reason).toBeTruthy(); } + }); - // The test passes regardless of dead code count - we just want to verify the flow works - expect(Array.isArray(deadCode)).toBe(true); - }, 60000); + it('should support client-side ignore-patterns filtering', () => { + const all = result.deadCodeCandidates; + // Even if no dead code exists, verify the filter runs without error + const filtered = filterByIgnorePatterns(all, ['**/nonexistent/**']); + expect(filtered).toHaveLength(all.length); + + // Filtering with a broad pattern should return fewer or equal results + const aggressive = filterByIgnorePatterns(all, ['**/*.ts']); + expect(aggressive.length).toBeLessThanOrEqual(all.length); + }); }); describe('Integration Test Prerequisites', () => { it('should have SUPERMODEL_API_KEY to run integration tests', () => { if (SKIP_INTEGRATION) { - console.log('⚠️ SUPERMODEL_API_KEY not set - skipping integration tests'); + console.log('SUPERMODEL_API_KEY not set - skipping integration tests'); console.log(' Set the environment variable to run integration tests'); } else { - console.log('✓ SUPERMODEL_API_KEY is set'); + console.log('SUPERMODEL_API_KEY is set'); } - expect(true).toBe(true); // Always passes + expect(true).toBe(true); }); }); diff --git a/src/dead-code.ts b/src/dead-code.ts index a1ec4f6..d93c964 100644 --- a/src/dead-code.ts +++ b/src/dead-code.ts @@ -1,187 +1,74 @@ import { minimatch } from 'minimatch'; -import { CodeGraphNode, CodeGraphRelationship } from '@supermodeltools/sdk'; +import type { DeadCodeCandidate, DeadCodeAnalysisResponse, DeadCodeAnalysisMetadata } from '@supermodeltools/sdk'; +import { escapeTableCell } from './markdown'; -/** - * Represents a potentially unused function found in the codebase. - */ -export interface DeadCodeResult { - id: string; - name: string; - filePath: string; - startLine?: number; - endLine?: number; -} - -/** Default glob patterns for files to exclude from dead code analysis. */ -export const DEFAULT_EXCLUDE_PATTERNS = [ - '**/node_modules/**', - '**/dist/**', - '**/build/**', - '**/.git/**', - '**/vendor/**', - '**/target/**', - '**/*.test.ts', - '**/*.test.tsx', - '**/*.test.js', - '**/*.test.jsx', - '**/*.spec.ts', - '**/*.spec.tsx', - '**/*.spec.js', - '**/*.spec.jsx', - '**/__tests__/**', - '**/__mocks__/**', -]; - -/** Glob patterns for files that are considered entry points. */ -export const ENTRY_POINT_PATTERNS = [ - '**/index.ts', - '**/index.js', - '**/main.ts', - '**/main.js', - '**/app.ts', - '**/app.js', - '**/*.test.*', - '**/*.spec.*', - '**/__tests__/**', -]; - -/** Function names that are considered entry points. */ -export const ENTRY_POINT_FUNCTION_NAMES = [ - 'main', - 'run', - 'start', - 'init', - 'setup', - 'bootstrap', - 'default', - 'handler', - 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', -]; - -/** - * Checks if a file path matches any entry point pattern. - * @param filePath - The file path to check - * @returns True if the file is an entry point - */ -export function isEntryPointFile(filePath: string): boolean { - return ENTRY_POINT_PATTERNS.some(pattern => minimatch(filePath, pattern)); -} +export type { DeadCodeCandidate, DeadCodeAnalysisResponse, DeadCodeAnalysisMetadata }; /** - * Checks if a function name is a common entry point name. - * @param name - The function name to check - * @returns True if the function name is an entry point + * Filters dead code candidates by user-provided ignore patterns. + * The API handles all analysis server-side; this is purely for + * client-side post-filtering on file paths. */ -export function isEntryPointFunction(name: string): boolean { - const lowerName = name.toLowerCase(); - return ENTRY_POINT_FUNCTION_NAMES.some(ep => lowerName === ep.toLowerCase()); -} - -/** - * Checks if a file should be ignored based on exclude patterns. - * @param filePath - The file path to check - * @param ignorePatterns - Additional patterns to ignore - * @returns True if the file should be ignored - */ -export function shouldIgnoreFile(filePath: string, ignorePatterns: string[] = []): boolean { - const allPatterns = [...DEFAULT_EXCLUDE_PATTERNS, ...ignorePatterns]; - return allPatterns.some(pattern => minimatch(filePath, pattern)); -} - -/** - * Analyzes a code graph to find functions that are never called. - * @param nodes - All nodes from the code graph - * @param relationships - All relationships from the code graph - * @param ignorePatterns - Additional glob patterns to ignore - * @returns Array of potentially unused functions - */ -export function findDeadCode( - nodes: CodeGraphNode[], - relationships: CodeGraphRelationship[], - ignorePatterns: string[] = [] -): DeadCodeResult[] { - const functionNodes = nodes.filter(node => - node.labels?.includes('Function') - ); - - const callRelationships = relationships.filter(rel => rel.type === 'calls'); - const calledFunctionIds = new Set(callRelationships.map(rel => rel.endNode)); - - const deadCode: DeadCodeResult[] = []; - - for (const node of functionNodes) { - const props = node.properties || {}; - const filePath = props.filePath || props.file || ''; - const name = props.name || 'anonymous'; - - if (calledFunctionIds.has(node.id)) { - continue; - } - - if (shouldIgnoreFile(filePath, ignorePatterns)) { - continue; - } - - if (isEntryPointFile(filePath)) { - continue; - } - - if (isEntryPointFunction(name)) { - continue; - } - - if (props.exported === true || props.isExported === true) { - continue; - } - - deadCode.push({ - id: node.id, - name, - filePath, - startLine: props.startLine, - endLine: props.endLine, - }); - } - - return deadCode; +export function filterByIgnorePatterns( + candidates: DeadCodeCandidate[], + ignorePatterns: string[] +): DeadCodeCandidate[] { + if (ignorePatterns.length === 0) return candidates; + return candidates.filter(c => !ignorePatterns.some(p => minimatch(c.file, p))); } /** - * Formats dead code results as a GitHub PR comment. - * @param deadCode - Array of dead code results - * @returns Markdown-formatted comment string + * Formats dead code analysis results as a GitHub PR comment. */ -export function formatPrComment(deadCode: DeadCodeResult[]): string { - if (deadCode.length === 0) { +export function formatPrComment( + candidates: DeadCodeCandidate[], + metadata?: DeadCodeAnalysisMetadata +): string { + if (candidates.length === 0) { return `## Dead Code Hunter No dead code found! Your codebase is clean.`; } - const rows = deadCode + const rows = candidates .slice(0, 50) .map(dc => { - const lineInfo = dc.startLine ? `L${dc.startLine}` : ''; - const fileLink = dc.startLine - ? `${dc.filePath}#L${dc.startLine}` - : dc.filePath; - return `| \`${dc.name}\` | ${fileLink} | ${lineInfo} |`; + const lineInfo = dc.line ? `L${dc.line}` : ''; + const fileLink = dc.line ? `${dc.file}#L${dc.line}` : dc.file; + const badge = dc.confidence === 'high' ? ':red_circle:' : + dc.confidence === 'medium' ? ':orange_circle:' : ':yellow_circle:'; + return `| \`${escapeTableCell(dc.name)}\` | ${dc.type} | ${fileLink} | ${lineInfo} | ${badge} ${dc.confidence} |`; }) .join('\n'); let comment = `## Dead Code Hunter -Found **${deadCode.length}** potentially unused function${deadCode.length === 1 ? '' : 's'}: +Found **${candidates.length}** potentially unused code element${candidates.length === 1 ? '' : 's'}: -| Function | File | Line | -|----------|------|------| +| Name | Type | File | Line | Confidence | +|------|------|------|------|------------| ${rows}`; - if (deadCode.length > 50) { - comment += `\n\n_...and ${deadCode.length - 50} more. See action output for full list._`; + if (candidates.length > 50) { + comment += `\n\n_...and ${candidates.length - 50} more. See action output for full list._`; + } + + if (metadata) { + comment += `\n\n
Analysis summary\n\n`; + comment += `- **Total declarations analyzed**: ${metadata.totalDeclarations}\n`; + comment += `- **Dead code candidates**: ${metadata.deadCodeCandidates}\n`; + comment += `- **Alive code**: ${metadata.aliveCode}\n`; + comment += `- **Analysis method**: ${metadata.analysisMethod}\n`; + if (metadata.transitiveDeadCount != null) { + comment += `- **Transitive dead**: ${metadata.transitiveDeadCount}\n`; + } + if (metadata.symbolLevelDeadCount != null) { + comment += `- **Symbol-level dead**: ${metadata.symbolLevelDeadCount}\n`; + } + comment += `\n
`; } - comment += `\n\n---\n_Powered by [Supermodel](https://supermodeltools.com) graph analysis_`; + comment += `\n\n---\n_Powered by [Supermodel](https://supermodeltools.com) dead code analysis_`; return comment; } diff --git a/src/index.ts b/src/index.ts index f9d35f3..59ab1e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,8 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import { randomUUID } from 'crypto'; import { Configuration, DefaultApi } from '@supermodeltools/sdk'; -import { findDeadCode, formatPrComment } from './dead-code'; +import type { DeadCodeAnalysisResponseAsync, DeadCodeAnalysisResponse } from '@supermodeltools/sdk'; +import { filterByIgnorePatterns, formatPrComment } from './dead-code'; /** Fields that should be redacted from logs */ const SENSITIVE_KEYS = new Set([ @@ -23,6 +24,9 @@ const SENSITIVE_KEYS = new Set([ ]); const MAX_VALUE_LENGTH = 1000; +const MAX_POLL_ATTEMPTS = 90; +const DEFAULT_RETRY_INTERVAL_MS = 10_000; +const POLL_TIMEOUT_MS = 15 * 60 * 1000; /** * Safely serialize a value for logging, handling circular refs, BigInt, and large values. @@ -33,28 +37,21 @@ function safeSerialize(value: unknown, maxLength = MAX_VALUE_LENGTH): string { const seen = new WeakSet(); const serialized = JSON.stringify(value, (key, val) => { - // Redact sensitive keys if (key && SENSITIVE_KEYS.has(key.toLowerCase())) { return '[REDACTED]'; } - - // Handle BigInt if (typeof val === 'bigint') { return val.toString(); } - - // Handle circular references if (typeof val === 'object' && val !== null) { if (seen.has(val)) { return '[Circular]'; } seen.add(val); } - return val; }, 2); - // Truncate if too long if (serialized && serialized.length > maxLength) { return serialized.slice(0, maxLength) + '... [truncated]'; } @@ -114,8 +111,50 @@ async function generateIdempotencyKey(workspacePath: string): Promise { const commitHash = output.trim(); const repoName = path.basename(workspacePath); - // Use UUID to ensure unique key per run (avoids 409 conflicts, scales to many concurrent users) - return `${repoName}:deadcode:${commitHash}:${randomUUID()}`; + return `${repoName}:analysis:deadcode:${commitHash}:${randomUUID()}`; +} + +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Polls the dead code analysis endpoint until the job completes or fails. + * The API returns 202 while processing; re-submitting the same request + * with the same idempotency key acts as a poll. + */ +async function pollForResult( + api: DefaultApi, + idempotencyKey: string, + zipBlob: Blob +): Promise { + const startTime = Date.now(); + + for (let attempt = 1; attempt <= MAX_POLL_ATTEMPTS; attempt++) { + const response: DeadCodeAnalysisResponseAsync = await api.generateDeadCodeAnalysis({ + idempotencyKey, + file: zipBlob, + }); + + if (response.status === 'completed' && response.result) { + return response.result; + } + + if (response.status === 'failed') { + throw new Error(`Analysis job failed: ${response.error || 'unknown error'}`); + } + + const elapsed = Date.now() - startTime; + if (elapsed >= POLL_TIMEOUT_MS) { + throw new Error(`Analysis timed out after ${Math.round(elapsed / 1000)}s (job: ${response.jobId})`); + } + + const retryMs = (response.retryAfter ?? DEFAULT_RETRY_INTERVAL_MS / 1000) * 1000; + core.info(`Job ${response.jobId} status: ${response.status} (attempt ${attempt}/${MAX_POLL_ATTEMPTS}, retry in ${retryMs / 1000}s)`); + await sleep(retryMs); + } + + throw new Error(`Analysis did not complete within ${MAX_POLL_ATTEMPTS} polling attempts`); } async function run(): Promise { @@ -128,7 +167,7 @@ async function run(): Promise { const commentOnPr = core.getBooleanInput('comment-on-pr'); const failOnDeadCode = core.getBooleanInput('fail-on-dead-code'); - const ignorePatterns = JSON.parse(core.getInput('ignore-patterns') || '[]'); + const ignorePatterns: string[] = JSON.parse(core.getInput('ignore-patterns') || '[]'); const workspacePath = process.env.GITHUB_WORKSPACE || process.cwd(); @@ -140,7 +179,7 @@ async function run(): Promise { // Step 2: Generate idempotency key const idempotencyKey = await generateIdempotencyKey(workspacePath); - // Step 3: Call Supermodel API + // Step 3: Call Supermodel dead code analysis API core.info('Analyzing codebase with Supermodel...'); const config = new Configuration({ @@ -153,29 +192,28 @@ async function run(): Promise { const zipBuffer = await fs.readFile(zipPath); const zipBlob = new Blob([zipBuffer], { type: 'application/zip' }); - const response = await api.generateSupermodelGraph({ - idempotencyKey, - file: zipBlob, - }); - - // Step 4: Analyze for dead code - const nodes = response.graph?.nodes || []; - const relationships = response.graph?.relationships || []; + const result = await pollForResult(api, idempotencyKey, zipBlob); - const deadCode = findDeadCode(nodes, relationships, ignorePatterns); + // Step 4: Apply client-side ignore patterns + const candidates = filterByIgnorePatterns(result.deadCodeCandidates, ignorePatterns); - core.info(`Found ${deadCode.length} potentially unused functions`); + core.info(`Found ${candidates.length} potentially unused code elements (${result.metadata.totalDeclarations} declarations analyzed)`); + core.info(`Analysis method: ${result.metadata.analysisMethod}`); + core.info(`Alive: ${result.metadata.aliveCode}, Entry points: ${result.entryPoints.length}, Root files: ${result.metadata.rootFilesCount ?? 'n/a'}`); + for (const dc of candidates) { + core.info(` [${dc.confidence}] ${dc.type} ${dc.name} @ ${dc.file}:${dc.line} — ${dc.reason}`); + } // Step 5: Set outputs - core.setOutput('dead-code-count', deadCode.length); - core.setOutput('dead-code-json', JSON.stringify(deadCode)); + core.setOutput('dead-code-count', candidates.length); + core.setOutput('dead-code-json', JSON.stringify(candidates)); // Step 6: Post PR comment if enabled if (commentOnPr && github.context.payload.pull_request) { const token = core.getInput('github-token') || process.env.GITHUB_TOKEN; if (token) { const octokit = github.getOctokit(token); - const comment = formatPrComment(deadCode); + const comment = formatPrComment(candidates, result.metadata); await octokit.rest.issues.createComment({ owner: github.context.repo.owner, @@ -194,25 +232,21 @@ async function run(): Promise { await fs.unlink(zipPath); // Step 8: Fail if configured and dead code found - if (deadCode.length > 0 && failOnDeadCode) { - core.setFailed(`Found ${deadCode.length} potentially unused functions`); + if (candidates.length > 0 && failOnDeadCode) { + core.setFailed(`Found ${candidates.length} potentially unused code elements`); } } catch (error: any) { - // Log error details for debugging (using debug level for potentially sensitive data) core.info('--- Error Debug Info ---'); core.info(`Error type: ${error?.constructor?.name ?? 'unknown'}`); core.info(`Error message: ${error?.message ?? 'no message'}`); core.info(`Error name: ${error?.name ?? 'no name'}`); - // Check various error structures used by different HTTP clients - // Use core.debug for detailed/sensitive info, core.info for safe summaries try { if (error?.response) { core.info(`Response status: ${error.response.status ?? 'unknown'}`); core.info(`Response statusText: ${error.response.statusText ?? 'unknown'}`); core.info(`Response data: ${safeSerialize(error.response.data)}`); - // Headers may contain sensitive values - use debug level core.debug(`Response headers: ${safeSerialize(redactSensitive(error.response.headers))}`); } if (error?.body) { @@ -235,11 +269,9 @@ async function run(): Promise { let errorMessage = 'An unknown error occurred'; let helpText = ''; - // Try multiple error structures const status = error?.response?.status || error?.status || error?.statusCode; let apiMessage = ''; - // Try to extract message from various locations try { apiMessage = error?.response?.data?.message || @@ -262,7 +294,6 @@ async function run(): Promise { } else if (status === 500) { errorMessage = apiMessage || 'Internal server error'; - // Check for common issues and provide guidance if (apiMessage.includes('Nested archives')) { helpText = 'Your repository contains nested archive files (.zip, .tar, etc.). ' + 'Add them to .gitattributes with "export-ignore" to exclude from analysis. ' + diff --git a/src/markdown.ts b/src/markdown.ts new file mode 100644 index 0000000..7c6e871 --- /dev/null +++ b/src/markdown.ts @@ -0,0 +1,6 @@ +/** + * Escapes pipe characters for safe rendering inside markdown tables. + */ +export function escapeTableCell(text: string): string { + return text.replace(/\|/g, '\\|').replace(/\n/g, ' '); +}