From 510e3c7145897a2fdae166932f85f23e6510a12d Mon Sep 17 00:00:00 2001 From: jonathanpopham Date: Tue, 10 Feb 2026 20:22:39 -0500 Subject: [PATCH 1/4] refactor: consume /v1/analysis/dead-code API endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace local graph analysis with the dedicated dead code analysis endpoint. The API now handles parse graph + call graph combination, transitive dead code propagation, symbol-level import analysis, entry point detection, and confidence scoring server-side. - Switch from generateSupermodelGraph to generateDeadCodeAnalysis - Add async polling for job completion (202 → poll → 200) - Remove local findDeadCode, entry point heuristics, pattern constants - Keep filterByIgnorePatterns for client-side post-filtering - Enrich PR comment with Type, Confidence columns and metadata summary - Bump @supermodeltools/sdk ^0.4.1 → ^0.9.3 - Bump version 0.1.0 → 0.2.0 Closes #8 --- dist/dead-code.d.ts | 54 +- dist/dead-code.d.ts.map | 2 +- dist/index.d.ts.map | 2 +- dist/index.js | 1674 +++++++++++++++++++++++++---- package-lock.json | 8 +- package.json | 6 +- src/__tests__/dead-code.test.ts | 335 ++---- src/__tests__/integration.test.ts | 107 +- src/dead-code.ts | 202 +--- src/index.ts | 94 +- 10 files changed, 1769 insertions(+), 715 deletions(-) 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..e6655b6 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;AAElH,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..05a76ae 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)), @@ -8049,20 +8930,157 @@ function DomainClassificationResponseFromJSONTyped(json, ignoreDiscriminator) { 'stats': (0, ClassificationStats_1.ClassificationStatsFromJSON)(json['stats']), }; } -function DomainClassificationResponseToJSON(value) { +function DomainClassificationResponseToJSON(value) { + if (value == null) { + return 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)), + 'functionAssignments': (value['functionAssignments'].map(DomainFunctionAssignment_1.DomainFunctionAssignmentToJSON)), + 'unassignedFunctions': (value['unassignedFunctions'].map(UnassignedFunction_1.UnassignedFunctionToJSON)), + 'classAssignments': (value['classAssignments'].map(DomainClassAssignment_1.DomainClassAssignmentToJSON)), + 'functionDescriptions': value['functionDescriptions'] == null ? undefined : (value['functionDescriptions'].map(FunctionDescription_1.FunctionDescriptionToJSON)), + 'stats': (0, ClassificationStats_1.ClassificationStatsToJSON)(value['stats']), + }; +} + + +/***/ }), + +/***/ 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 { - 'runId': value['runId'], - 'domains': (value['domains'].map(DomainSummary_1.DomainSummaryToJSON)), - 'relationships': (value['relationships'].map(DomainRelationship_1.DomainRelationshipToJSON)), - 'fileAssignments': (value['fileAssignments'].map(DomainFileAssignment_1.DomainFileAssignmentToJSON)), - 'functionAssignments': (value['functionAssignments'].map(DomainFunctionAssignment_1.DomainFunctionAssignmentToJSON)), - 'unassignedFunctions': (value['unassignedFunctions'].map(UnassignedFunction_1.UnassignedFunctionToJSON)), - 'classAssignments': (value['classAssignments'].map(DomainClassAssignment_1.DomainClassAssignmentToJSON)), - 'functionDescriptions': value['functionDescriptions'] == null ? undefined : (value['functionDescriptions'].map(FunctionDescription_1.FunctionDescriptionToJSON)), - 'stats': (0, ClassificationStats_1.ClassificationStatsToJSON)(value['stats']), + 'nodes': (value['nodes'].map(CodeGraphNode_1.CodeGraphNodeToJSON)), + 'relationships': (value['relationships'].map(CodeGraphRelationship_1.CodeGraphRelationshipToJSON)), }; } @@ -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,63 @@ 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)); -} -/** - * 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 - */ -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 + * 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 shouldIgnoreFile(filePath, ignorePatterns = []) { - const allPatterns = [...exports.DEFAULT_EXCLUDE_PATTERNS, ...ignorePatterns]; - return allPatterns.some(pattern => (0, minimatch_1.minimatch)(filePath, pattern)); +function filterByIgnorePatterns(candidates, ignorePatterns) { + if (ignorePatterns.length === 0) + return candidates; + return candidates.filter(c => !ignorePatterns.some(p => (0, minimatch_1.minimatch)(c.file, p))); } /** - * 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 + * Formats dead code analysis results as a GitHub PR comment. */ -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; -} -/** - * Formats dead code results as a GitHub PR comment. - * @param deadCode - Array of dead code results - * @returns Markdown-formatted comment string - */ -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 `| \`${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 +33757,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 +33768,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 +33782,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 +33832,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 +33880,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 +33889,19 @@ 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)`); // 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 +33917,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 +33952,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 +33976,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. ' + 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..a7fd81a 100644 --- a/src/__tests__/dead-code.test.ts +++ b/src/__tests__/dead-code.test.ts @@ -1,208 +1,61 @@ import { describe, it, expect } from 'vitest'; -import { - findDeadCode, - isEntryPointFile, - isEntryPointFunction, - shouldIgnoreFile, - formatPrComment, - DeadCodeResult, -} from '../dead-code'; -import { CodeGraphNode, CodeGraphRelationship } 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); - }); - - 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); - }); -}); - -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); - }); - - it('should be case-insensitive', () => { - expect(isEntryPointFunction('Main')).toBe(true); - expect(isEntryPointFunction('MAIN')).toBe(true); - expect(isEntryPointFunction('Handler')).toBe(true); - }); - - 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 not identify regular function names', () => { - expect(isEntryPointFunction('processData')).toBe(false); - expect(isEntryPointFunction('calculateTotal')).toBe(false); - }); -}); - -describe('shouldIgnoreFile', () => { - it('should ignore node_modules', () => { - expect(shouldIgnoreFile('node_modules/lodash/index.js')).toBe(true); - }); - - 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 ignore test files', () => { - expect(shouldIgnoreFile('src/utils.test.ts')).toBe(true); - expect(shouldIgnoreFile('src/utils.spec.js')).toBe(true); - }); - - it('should not ignore regular source files', () => { - expect(shouldIgnoreFile('src/utils.ts')).toBe(false); - expect(shouldIgnoreFile('lib/helpers.js')).toBe(false); - }); - - it('should respect custom ignore patterns', () => { - expect(shouldIgnoreFile('src/generated/api.ts', ['**/generated/**'])).toBe(true); - expect(shouldIgnoreFile('src/utils.ts', ['**/generated/**'])).toBe(false); - }); -}); - -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'); - }); - - 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); - - expect(deadCode).toHaveLength(0); - }); - - 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' } }, - ]; - - const relationships: CodeGraphRelationship[] = []; - - const deadCode = findDeadCode(nodes, relationships); - - expect(deadCode).toHaveLength(1); - expect(deadCode[0].name).toBe('notExported'); - }); - - 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); - - expect(deadCode).toHaveLength(1); - expect(deadCode[0].name).toBe('unusedHelper'); - }); - - 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); - - expect(deadCode).toHaveLength(1); - expect(deadCode[0].name).toBe('unusedHelper'); - }); - - 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' } }, +import { filterByIgnorePatterns, formatPrComment } from '../dead-code'; +import type { DeadCodeCandidate, DeadCodeAnalysisMetadata } from '@supermodeltools/sdk'; + +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, + }; +} + +function makeMetadata(overrides: Partial = {}): DeadCodeAnalysisMetadata { + return { + totalDeclarations: 100, + deadCodeCandidates: 5, + aliveCode: 95, + analysisMethod: 'parse_graph + call_graph', + ...overrides, + }; +} + +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 filter candidates matching ignore patterns', () => { + const candidates = [ + makeCandidate({ file: 'src/generated/api.ts' }), + makeCandidate({ file: 'src/utils.ts' }), ]; - - const relationships: CodeGraphRelationship[] = []; - - const deadCode = findDeadCode(nodes, relationships); - - expect(deadCode).toHaveLength(1); - expect(deadCode[0].name).toBe('unusedHelper'); + const result = filterByIgnorePatterns(candidates, ['**/generated/**']); + expect(result).toHaveLength(1); + expect(result[0].file).toBe('src/utils.ts'); }); - 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' } }, + 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 relationships: CodeGraphRelationship[] = []; - - const deadCode = findDeadCode(nodes, relationships); - - expect(deadCode).toHaveLength(1); - expect(deadCode[0].name).toBe('unusedFn'); + const result = filterByIgnorePatterns(candidates, ['**/generated/**', '**/migrations/**']); + expect(result).toHaveLength(1); + expect(result[0].file).toBe('src/utils.ts'); }); - 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 } }, - ]; - - const deadCode = findDeadCode(nodes, []); - - expect(deadCode[0].startLine).toBe(10); - expect(deadCode[0].endLine).toBe(20); + 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); }); }); @@ -213,49 +66,81 @@ describe('formatPrComment', () => { expect(comment).toContain('codebase is clean'); }); - it('should format single result', () => { - const deadCode: DeadCodeResult[] = [ - { id: 'fn1', name: 'unusedFn', filePath: 'src/utils.ts', startLine: 10 }, - ]; - - const comment = formatPrComment(deadCode); + it('should format single result with type and confidence', () => { + const candidates = [makeCandidate()]; + const comment = formatPrComment(candidates); - expect(comment).toContain('1** potentially unused function'); + 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 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 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 comment = formatPrComment(deadCode); - - expect(comment).toContain('2** potentially unused functions'); - expect(comment).toContain('`unusedFn1`'); - expect(comment).toContain('`unusedFn2`'); + expect(comment).toContain('2** potentially unused code elements'); + expect(comment).toContain('`fn1`'); + expect(comment).toContain('`fn2`'); + expect(comment).toContain('class'); }); 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`, - })); - - const comment = formatPrComment(deadCode); + const candidates = Array.from({ length: 60 }, (_, i) => + makeCandidate({ name: `fn${i}`, file: `src/file${i}.ts`, line: i + 1 }) + ); + const comment = formatPrComment(candidates); - expect(comment).toContain('60** potentially unused functions'); + expect(comment).toContain('60** potentially unused code elements'); 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' }, + it('should include metadata details section when provided', () => { + const candidates = [makeCandidate()]; + const metadata = makeMetadata({ transitiveDeadCount: 3, symbolLevelDeadCount: 7 }); + const comment = formatPrComment(candidates, metadata); + + 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 omit optional metadata fields when not present', () => { + const candidates = [makeCandidate()]; + const metadata = makeMetadata(); + const comment = formatPrComment(candidates, metadata); + + expect(comment).toContain('Analysis summary'); + expect(comment).not.toContain('Transitive dead'); + expect(comment).not.toContain('Symbol-level dead'); + }); + + 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(deadCode); + const comment = formatPrComment(candidates); + + expect(comment).toContain(':red_circle: high'); + expect(comment).toContain(':orange_circle: medium'); + expect(comment).toContain(':yellow_circle: low'); + }); + + it('should include Supermodel attribution', () => { + const candidates = [makeCandidate()]; + const comment = formatPrComment(candidates); expect(comment).toContain('Powered by [Supermodel]'); }); }); diff --git a/src/__tests__/integration.test.ts b/src/__tests__/integration.test.ts index 11dc794..954c8f4 100644 --- a/src/__tests__/integration.test.ts +++ b/src/__tests__/integration.test.ts @@ -3,11 +3,45 @@ 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; @@ -20,7 +54,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 +62,60 @@ 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:analysis:deadcode:${commitHash}`; }); - it('should call the Supermodel API and get a call graph', async () => { + it('should call the dead code analysis API and get results', async () => { const zipBuffer = await fs.readFile(zipPath); const zipBlob = new Blob([zipBuffer], { type: 'application/zip' }); - const response = await api.generateCallGraph({ - idempotencyKey, - file: zipBlob, - }); + const result = await pollForResult(api, idempotencyKey, zipBlob); - expect(response).toBeDefined(); - expect(response.graph).toBeDefined(); - expect(response.graph?.nodes).toBeDefined(); - expect(response.graph?.relationships).toBeDefined(); - expect(response.stats).toBeDefined(); + 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); - 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 + console.log('Metadata:', result.metadata); + console.log('Dead code candidates:', result.deadCodeCandidates.length); + console.log('Alive code:', result.aliveCode.length); + console.log('Entry points:', result.entryPoints.length); + }, 120_000); - it('should find dead code in the dead-code-hunter repo itself', async () => { + it('should analyze 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 || []; - - const deadCode = findDeadCode(nodes, relationships); + const result = await pollForResult(api, idempotencyKey, zipBlob); + const candidates = filterByIgnorePatterns(result.deadCodeCandidates, []); 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}`); - - 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 || '?'})`); + console.log(`Total declarations: ${result.metadata.totalDeclarations}`); + console.log(`Dead code candidates: ${candidates.length}`); + console.log(`Alive code: ${result.metadata.aliveCode}`); + console.log(`Analysis method: ${result.metadata.analysisMethod}`); + + if (candidates.length > 0) { + console.log('\nPotentially dead code:'); + for (const dc of candidates.slice(0, 10)) { + console.log(` - [${dc.confidence}] ${dc.name} (${dc.file}:${dc.line}) - ${dc.reason}`); } } - // The test passes regardless of dead code count - we just want to verify the flow works - expect(Array.isArray(deadCode)).toBe(true); - }, 60000); + expect(Array.isArray(candidates)).toBe(true); + }, 120_000); }); 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..3c9945a 100644 --- a/src/dead-code.ts +++ b/src/dead-code.ts @@ -1,187 +1,73 @@ import { minimatch } from 'minimatch'; -import { CodeGraphNode, CodeGraphRelationship } from '@supermodeltools/sdk'; +import type { DeadCodeCandidate, DeadCodeAnalysisResponse, DeadCodeAnalysisMetadata } from '@supermodeltools/sdk'; -/** - * 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 `| \`${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..2ecf4fa 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,23 @@ 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)`); // 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 +227,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 +264,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 +289,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. ' + From f6fd34f7858213a5ca05c4c5dfd6ac5f49cbdb50 Mon Sep 17 00:00:00 2001 From: jonathanpopham Date: Tue, 10 Feb 2026 20:26:29 -0500 Subject: [PATCH 2/4] test: add intentional dead code and integration tests that verify detection Add uncalled functions in dead-code.ts (truncateString, groupByDirectory, fileSeverity) and a new markdown.ts module (badge, barChart, numberedList, collapsible, escapeTableCell) to verify the API actually finds dead code. Integration tests now assert: - Response shape is valid (metadata, candidates, aliveCode, entryPoints) - Dead code count > 0 - Every candidate has file, name, line, type, confidence, reason - Known dead functions are found by name (at least 3 of 6) - ignore-patterns filtering works against real results --- dist/dead-code.d.ts | 12 +++++ dist/dead-code.d.ts.map | 2 +- dist/index.js | 38 ++++++++++++++++ dist/markdown.d.ts | 23 ++++++++++ dist/markdown.d.ts.map | 1 + src/__tests__/integration.test.ts | 74 ++++++++++++++++++++----------- src/dead-code.ts | 34 ++++++++++++++ src/markdown.ts | 42 ++++++++++++++++++ 8 files changed, 200 insertions(+), 26 deletions(-) create mode 100644 dist/markdown.d.ts create mode 100644 dist/markdown.d.ts.map create mode 100644 src/markdown.ts diff --git a/dist/dead-code.d.ts b/dist/dead-code.d.ts index 2e2a7ca..6a42a0c 100644 --- a/dist/dead-code.d.ts +++ b/dist/dead-code.d.ts @@ -1,5 +1,17 @@ import type { DeadCodeCandidate, DeadCodeAnalysisResponse, DeadCodeAnalysisMetadata } from '@supermodeltools/sdk'; export type { DeadCodeCandidate, DeadCodeAnalysisResponse, DeadCodeAnalysisMetadata }; +/** + * Truncates a string to the given max length, appending an ellipsis if needed. + */ +export declare function truncateString(str: string, maxLen: number): string; +/** + * Groups candidates by their containing directory. + */ +export declare function groupByDirectory(candidates: DeadCodeCandidate[]): Map; +/** + * Returns a severity label based on how many dead code items are in a single file. + */ +export declare function fileSeverity(count: number): 'clean' | 'warning' | 'critical'; /** * Filters dead code candidates by user-provided ignore patterns. * The API handles all analysis server-side; this is purely for diff --git a/dist/dead-code.d.ts.map b/dist/dead-code.d.ts.map index e6655b6..a833261 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/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;AAElH,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 +{"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;AAElH,YAAY,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,CAAC;AAEtF;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGlE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,iBAAiB,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,CAYlG;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,CAI5E;AAED;;;;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.js b/dist/index.js index 05a76ae..57365e5 100644 --- a/dist/index.js +++ b/dist/index.js @@ -33632,9 +33632,47 @@ function wrappy (fn, cb) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.truncateString = truncateString; +exports.groupByDirectory = groupByDirectory; +exports.fileSeverity = fileSeverity; exports.filterByIgnorePatterns = filterByIgnorePatterns; exports.formatPrComment = formatPrComment; const minimatch_1 = __nccwpck_require__(6507); +/** + * Truncates a string to the given max length, appending an ellipsis if needed. + */ +function truncateString(str, maxLen) { + if (str.length <= maxLen) + return str; + return str.slice(0, maxLen - 1) + '\u2026'; +} +/** + * Groups candidates by their containing directory. + */ +function groupByDirectory(candidates) { + const groups = new Map(); + for (const c of candidates) { + const dir = c.file.includes('/') ? c.file.slice(0, c.file.lastIndexOf('/')) : '.'; + const existing = groups.get(dir); + if (existing) { + existing.push(c); + } + else { + groups.set(dir, [c]); + } + } + return groups; +} +/** + * Returns a severity label based on how many dead code items are in a single file. + */ +function fileSeverity(count) { + if (count === 0) + return 'clean'; + if (count <= 3) + return 'warning'; + return 'critical'; +} /** * Filters dead code candidates by user-provided ignore patterns. * The API handles all analysis server-side; this is purely for diff --git a/dist/markdown.d.ts b/dist/markdown.d.ts new file mode 100644 index 0000000..73b0c15 --- /dev/null +++ b/dist/markdown.d.ts @@ -0,0 +1,23 @@ +/** + * Markdown formatting utilities for PR comments. + */ +/** + * Wraps text in a collapsible details/summary block. + */ +export declare function collapsible(summary: string, body: string): string; +/** + * Creates a markdown badge image link. + */ +export declare function badge(label: string, value: string, color: string): string; +/** + * Renders a horizontal bar chart using unicode block characters. + */ +export declare function barChart(value: number, max: number, width?: number): string; +/** + * Escapes pipe characters for safe rendering inside markdown tables. + */ +export declare function escapeTableCell(text: string): string; +/** + * Converts a flat list into a markdown numbered list. + */ +export declare function numberedList(items: string[]): string; diff --git a/dist/markdown.d.ts.map b/dist/markdown.d.ts.map new file mode 100644 index 0000000..9effc7e --- /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;AAEH;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIzE;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,MAAM,CAIvE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAEpD"} \ No newline at end of file diff --git a/src/__tests__/integration.test.ts b/src/__tests__/integration.test.ts index 954c8f4..7859691 100644 --- a/src/__tests__/integration.test.ts +++ b/src/__tests__/integration.test.ts @@ -46,6 +46,7 @@ describe.skipIf(SKIP_INTEGRATION)('Integration Tests', () => { let api: DefaultApi; let zipPath: string; let idempotencyKey: string; + let result: DeadCodeAnalysisResponse; beforeAll(async () => { const config = new Configuration({ @@ -62,50 +63,73 @@ describe.skipIf(SKIP_INTEGRATION)('Integration Tests', () => { const commitHash = execSync('git rev-parse --short HEAD', { cwd: repoRoot }) .toString() .trim(); - idempotencyKey = `dead-code-hunter:analysis:deadcode:${commitHash}`; - }); + idempotencyKey = `dead-code-hunter:integration:${commitHash}`; - it('should call the dead code analysis API and get results', async () => { const zipBuffer = await fs.readFile(zipPath); const zipBlob = new Blob([zipBuffer], { type: 'application/zip' }); + result = await pollForResult(api, idempotencyKey, zipBlob); + }, 120_000); - const result = await pollForResult(api, idempotencyKey, zipBlob); - + 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'); + }); - console.log('Metadata:', result.metadata); - console.log('Dead code candidates:', result.deadCodeCandidates.length); - console.log('Alive code:', result.aliveCode.length); - console.log('Entry points:', result.entryPoints.length); - }, 120_000); - - it('should analyze 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 result = await pollForResult(api, idempotencyKey, zipBlob); - const candidates = filterByIgnorePatterns(result.deadCodeCandidates, []); + it('should detect dead code in this repo', () => { + // We intentionally have uncalled functions in dead-code.ts and markdown.ts + const candidates = result.deadCodeCandidates; - console.log('\n=== Dead Code Hunter Self-Analysis ==='); + console.log('\n=== Dead Code Analysis Results ==='); console.log(`Total declarations: ${result.metadata.totalDeclarations}`); console.log(`Dead code candidates: ${candidates.length}`); console.log(`Alive code: ${result.metadata.aliveCode}`); console.log(`Analysis method: ${result.metadata.analysisMethod}`); - if (candidates.length > 0) { - console.log('\nPotentially dead code:'); - for (const dc of candidates.slice(0, 10)) { - console.log(` - [${dc.confidence}] ${dc.name} (${dc.file}:${dc.line}) - ${dc.reason}`); - } + for (const dc of candidates) { + console.log(` [${dc.confidence}] ${dc.type} ${dc.name} @ ${dc.file}:${dc.line} — ${dc.reason}`); } - expect(Array.isArray(candidates)).toBe(true); - }, 120_000); + expect(candidates.length).toBeGreaterThan(0); + }); + + it('should include file, name, line, type, confidence, and reason 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(); + } + }); + + it('should find known dead functions by name', () => { + const names = result.deadCodeCandidates.map(c => c.name); + + // These exist in src/dead-code.ts and src/markdown.ts but are never called + const knownDead = ['truncateString', 'groupByDirectory', 'fileSeverity', + 'badge', 'barChart', 'numberedList']; + const found = knownDead.filter(n => names.includes(n)); + + console.log(`\nKnown dead functions found: ${found.join(', ')}`); + console.log(`Known dead functions missed: ${knownDead.filter(n => !names.includes(n)).join(', ') || 'none'}`); + + // At least some of our intentionally dead code should be detected + expect(found.length).toBeGreaterThanOrEqual(3); + }); + + it('should respect ignore-patterns filtering', () => { + const all = result.deadCodeCandidates; + const filtered = filterByIgnorePatterns(all, ['**/markdown.ts']); + + expect(filtered.length).toBeLessThan(all.length); + expect(filtered.every(c => c.file !== 'src/markdown.ts')).toBe(true); + }); }); describe('Integration Test Prerequisites', () => { diff --git a/src/dead-code.ts b/src/dead-code.ts index 3c9945a..792b74f 100644 --- a/src/dead-code.ts +++ b/src/dead-code.ts @@ -3,6 +3,40 @@ import type { DeadCodeCandidate, DeadCodeAnalysisResponse, DeadCodeAnalysisMetad export type { DeadCodeCandidate, DeadCodeAnalysisResponse, DeadCodeAnalysisMetadata }; +/** + * Truncates a string to the given max length, appending an ellipsis if needed. + */ +export function truncateString(str: string, maxLen: number): string { + if (str.length <= maxLen) return str; + return str.slice(0, maxLen - 1) + '\u2026'; +} + +/** + * Groups candidates by their containing directory. + */ +export function groupByDirectory(candidates: DeadCodeCandidate[]): Map { + const groups = new Map(); + for (const c of candidates) { + const dir = c.file.includes('/') ? c.file.slice(0, c.file.lastIndexOf('/')) : '.'; + const existing = groups.get(dir); + if (existing) { + existing.push(c); + } else { + groups.set(dir, [c]); + } + } + return groups; +} + +/** + * Returns a severity label based on how many dead code items are in a single file. + */ +export function fileSeverity(count: number): 'clean' | 'warning' | 'critical' { + if (count === 0) return 'clean'; + if (count <= 3) return 'warning'; + return 'critical'; +} + /** * Filters dead code candidates by user-provided ignore patterns. * The API handles all analysis server-side; this is purely for diff --git a/src/markdown.ts b/src/markdown.ts new file mode 100644 index 0000000..f34a4c2 --- /dev/null +++ b/src/markdown.ts @@ -0,0 +1,42 @@ +/** + * Markdown formatting utilities for PR comments. + */ + +/** + * Wraps text in a collapsible details/summary block. + */ +export function collapsible(summary: string, body: string): string { + return `
${summary}\n\n${body}\n\n
`; +} + +/** + * Creates a markdown badge image link. + */ +export function badge(label: string, value: string, color: string): string { + const encodedLabel = encodeURIComponent(label); + const encodedValue = encodeURIComponent(value); + return `![${label}](https://img.shields.io/badge/${encodedLabel}-${encodedValue}-${color})`; +} + +/** + * Renders a horizontal bar chart using unicode block characters. + */ +export function barChart(value: number, max: number, width = 20): string { + const filled = Math.round((value / max) * width); + const empty = width - filled; + return '\u2588'.repeat(filled) + '\u2591'.repeat(empty); +} + +/** + * Escapes pipe characters for safe rendering inside markdown tables. + */ +export function escapeTableCell(text: string): string { + return text.replace(/\|/g, '\\|').replace(/\n/g, ' '); +} + +/** + * Converts a flat list into a markdown numbered list. + */ +export function numberedList(items: string[]): string { + return items.map((item, i) => `${i + 1}. ${item}`).join('\n'); +} From 729f000bd27a4656d67c8410fd749344293544c9 Mon Sep 17 00:00:00 2001 From: jonathanpopham Date: Tue, 10 Feb 2026 20:30:28 -0500 Subject: [PATCH 3/4] test: import markdown.ts into the dependency graph to test detection markdown.ts was never imported, so the API treated it as a root file (entry point) and didn't flag its exports. Now dead-code.ts imports and uses escapeTableCell, pulling markdown.ts into the import graph. The remaining unused exports (badge, barChart, collapsible, numberedList) should now be flagged as dead via symbol-level import analysis. Also adds diagnostic logging of full analysis results to CI output. --- dist/dead-code.d.ts.map | 2 +- dist/index.js | 60 ++++++++++++++++++++++++++++++++++++++++- src/dead-code.ts | 3 ++- src/index.ts | 5 ++++ 4 files changed, 67 insertions(+), 3 deletions(-) diff --git a/dist/dead-code.d.ts.map b/dist/dead-code.d.ts.map index a833261..3859ded 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/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;AAElH,YAAY,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,CAAC;AAEtF;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGlE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,iBAAiB,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,CAYlG;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,CAI5E;AAED;;;;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 +{"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;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGlE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,iBAAiB,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,CAYlG;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,CAI5E;AAED;;;;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.js b/dist/index.js index 57365e5..aebce92 100644 --- a/dist/index.js +++ b/dist/index.js @@ -33638,6 +33638,7 @@ exports.fileSeverity = fileSeverity; exports.filterByIgnorePatterns = filterByIgnorePatterns; exports.formatPrComment = formatPrComment; const minimatch_1 = __nccwpck_require__(6507); +const markdown_1 = __nccwpck_require__(3758); /** * Truncates a string to the given max length, appending an ellipsis if needed. */ @@ -33699,7 +33700,7 @@ No dead code found! Your codebase is clean.`; 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 `| \`${dc.name}\` | ${dc.type} | ${fileLink} | ${lineInfo} | ${badge} ${dc.confidence} |`; + return `| \`${(0, markdown_1.escapeTableCell)(dc.name)}\` | ${dc.type} | ${fileLink} | ${lineInfo} | ${badge} ${dc.confidence} |`; }) .join('\n'); let comment = `## Dead Code Hunter @@ -33931,6 +33932,11 @@ async function run() { // 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', candidates.length); core.setOutput('dead-code-json', JSON.stringify(candidates)); @@ -34048,6 +34054,58 @@ async function run() { run(); +/***/ }), + +/***/ 3758: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +/** + * Markdown formatting utilities for PR comments. + */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.collapsible = collapsible; +exports.badge = badge; +exports.barChart = barChart; +exports.escapeTableCell = escapeTableCell; +exports.numberedList = numberedList; +/** + * Wraps text in a collapsible details/summary block. + */ +function collapsible(summary, body) { + return `
${summary}\n\n${body}\n\n
`; +} +/** + * Creates a markdown badge image link. + */ +function badge(label, value, color) { + const encodedLabel = encodeURIComponent(label); + const encodedValue = encodeURIComponent(value); + return `![${label}](https://img.shields.io/badge/${encodedLabel}-${encodedValue}-${color})`; +} +/** + * Renders a horizontal bar chart using unicode block characters. + */ +function barChart(value, max, width = 20) { + const filled = Math.round((value / max) * width); + const empty = width - filled; + return '\u2588'.repeat(filled) + '\u2591'.repeat(empty); +} +/** + * Escapes pipe characters for safe rendering inside markdown tables. + */ +function escapeTableCell(text) { + return text.replace(/\|/g, '\\|').replace(/\n/g, ' '); +} +/** + * Converts a flat list into a markdown numbered list. + */ +function numberedList(items) { + return items.map((item, i) => `${i + 1}. ${item}`).join('\n'); +} + + /***/ }), /***/ 2613: diff --git a/src/dead-code.ts b/src/dead-code.ts index 792b74f..67773c0 100644 --- a/src/dead-code.ts +++ b/src/dead-code.ts @@ -1,5 +1,6 @@ import { minimatch } from 'minimatch'; import type { DeadCodeCandidate, DeadCodeAnalysisResponse, DeadCodeAnalysisMetadata } from '@supermodeltools/sdk'; +import { escapeTableCell } from './markdown'; export type { DeadCodeCandidate, DeadCodeAnalysisResponse, DeadCodeAnalysisMetadata }; @@ -70,7 +71,7 @@ No dead code found! Your codebase is clean.`; 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 `| \`${dc.name}\` | ${dc.type} | ${fileLink} | ${lineInfo} | ${badge} ${dc.confidence} |`; + return `| \`${escapeTableCell(dc.name)}\` | ${dc.type} | ${fileLink} | ${lineInfo} | ${badge} ${dc.confidence} |`; }) .join('\n'); diff --git a/src/index.ts b/src/index.ts index 2ecf4fa..59ab1e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -198,6 +198,11 @@ async function run(): Promise { const candidates = 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', candidates.length); From 065d283ffd0b8c8e3b0a3de67386d46d477cdfc2 Mon Sep 17 00:00:00 2001 From: jonathanpopham Date: Tue, 10 Feb 2026 21:50:34 -0500 Subject: [PATCH 4/4] clean up: remove intentional dead code, update docs and tests Remove test dead code (truncateString, groupByDirectory, fileSeverity, collapsible, badge, barChart, numberedList) that was used to verify orphaned file detection. Update README and action.yml to reflect the new API-driven architecture with confidence levels and broader type detection. Add escapeTableCell and code type coverage to unit tests. --- README.md | 56 ++++++++++++++---------- action.yml | 6 +-- dist/dead-code.d.ts | 12 ----- dist/dead-code.d.ts.map | 2 +- dist/index.js | 73 ------------------------------- dist/markdown.d.ts | 19 -------- dist/markdown.d.ts.map | 2 +- src/__tests__/dead-code.test.ts | 57 ++++++++++++++++++++++++ src/__tests__/integration.test.ts | 39 +++++------------ src/dead-code.ts | 34 -------------- src/markdown.ts | 36 --------------- 11 files changed, 107 insertions(+), 229 deletions(-) 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 6a42a0c..2e2a7ca 100644 --- a/dist/dead-code.d.ts +++ b/dist/dead-code.d.ts @@ -1,17 +1,5 @@ import type { DeadCodeCandidate, DeadCodeAnalysisResponse, DeadCodeAnalysisMetadata } from '@supermodeltools/sdk'; export type { DeadCodeCandidate, DeadCodeAnalysisResponse, DeadCodeAnalysisMetadata }; -/** - * Truncates a string to the given max length, appending an ellipsis if needed. - */ -export declare function truncateString(str: string, maxLen: number): string; -/** - * Groups candidates by their containing directory. - */ -export declare function groupByDirectory(candidates: DeadCodeCandidate[]): Map; -/** - * Returns a severity label based on how many dead code items are in a single file. - */ -export declare function fileSeverity(count: number): 'clean' | 'warning' | 'critical'; /** * Filters dead code candidates by user-provided ignore patterns. * The API handles all analysis server-side; this is purely for diff --git a/dist/dead-code.d.ts.map b/dist/dead-code.d.ts.map index 3859ded..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/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;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGlE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,iBAAiB,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,CAYlG;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,CAI5E;AAED;;;;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 +{"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.js b/dist/index.js index aebce92..73d82ed 100644 --- a/dist/index.js +++ b/dist/index.js @@ -33632,48 +33632,10 @@ function wrappy (fn, cb) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.truncateString = truncateString; -exports.groupByDirectory = groupByDirectory; -exports.fileSeverity = fileSeverity; exports.filterByIgnorePatterns = filterByIgnorePatterns; exports.formatPrComment = formatPrComment; const minimatch_1 = __nccwpck_require__(6507); const markdown_1 = __nccwpck_require__(3758); -/** - * Truncates a string to the given max length, appending an ellipsis if needed. - */ -function truncateString(str, maxLen) { - if (str.length <= maxLen) - return str; - return str.slice(0, maxLen - 1) + '\u2026'; -} -/** - * Groups candidates by their containing directory. - */ -function groupByDirectory(candidates) { - const groups = new Map(); - for (const c of candidates) { - const dir = c.file.includes('/') ? c.file.slice(0, c.file.lastIndexOf('/')) : '.'; - const existing = groups.get(dir); - if (existing) { - existing.push(c); - } - else { - groups.set(dir, [c]); - } - } - return groups; -} -/** - * Returns a severity label based on how many dead code items are in a single file. - */ -function fileSeverity(count) { - if (count === 0) - return 'clean'; - if (count <= 3) - return 'warning'; - return 'critical'; -} /** * Filters dead code candidates by user-provided ignore patterns. * The API handles all analysis server-side; this is purely for @@ -34061,49 +34023,14 @@ run(); "use strict"; -/** - * Markdown formatting utilities for PR comments. - */ Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.collapsible = collapsible; -exports.badge = badge; -exports.barChart = barChart; exports.escapeTableCell = escapeTableCell; -exports.numberedList = numberedList; -/** - * Wraps text in a collapsible details/summary block. - */ -function collapsible(summary, body) { - return `
${summary}\n\n${body}\n\n
`; -} -/** - * Creates a markdown badge image link. - */ -function badge(label, value, color) { - const encodedLabel = encodeURIComponent(label); - const encodedValue = encodeURIComponent(value); - return `![${label}](https://img.shields.io/badge/${encodedLabel}-${encodedValue}-${color})`; -} -/** - * Renders a horizontal bar chart using unicode block characters. - */ -function barChart(value, max, width = 20) { - const filled = Math.round((value / max) * width); - const empty = width - filled; - return '\u2588'.repeat(filled) + '\u2591'.repeat(empty); -} /** * Escapes pipe characters for safe rendering inside markdown tables. */ function escapeTableCell(text) { return text.replace(/\|/g, '\\|').replace(/\n/g, ' '); } -/** - * Converts a flat list into a markdown numbered list. - */ -function numberedList(items) { - return items.map((item, i) => `${i + 1}. ${item}`).join('\n'); -} /***/ }), diff --git a/dist/markdown.d.ts b/dist/markdown.d.ts index 73b0c15..f5d883b 100644 --- a/dist/markdown.d.ts +++ b/dist/markdown.d.ts @@ -1,23 +1,4 @@ -/** - * Markdown formatting utilities for PR comments. - */ -/** - * Wraps text in a collapsible details/summary block. - */ -export declare function collapsible(summary: string, body: string): string; -/** - * Creates a markdown badge image link. - */ -export declare function badge(label: string, value: string, color: string): string; -/** - * Renders a horizontal bar chart using unicode block characters. - */ -export declare function barChart(value: number, max: number, width?: number): string; /** * Escapes pipe characters for safe rendering inside markdown tables. */ export declare function escapeTableCell(text: string): string; -/** - * Converts a flat list into a markdown numbered list. - */ -export declare function numberedList(items: string[]): string; diff --git a/dist/markdown.d.ts.map b/dist/markdown.d.ts.map index 9effc7e..a0760d3 100644 --- a/dist/markdown.d.ts.map +++ b/dist/markdown.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"","sourceRoot":"","sources":["file:///Users/jag/repos/dead-code-hunter-issue-8/src/markdown.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIzE;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,MAAM,CAIvE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAEpD"} \ No newline at end of file +{"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/src/__tests__/dead-code.test.ts b/src/__tests__/dead-code.test.ts index a7fd81a..c966278 100644 --- a/src/__tests__/dead-code.test.ts +++ b/src/__tests__/dead-code.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest'; import { filterByIgnorePatterns, formatPrComment } from '../dead-code'; +import { escapeTableCell } from '../markdown'; import type { DeadCodeCandidate, DeadCodeAnalysisMetadata } from '@supermodeltools/sdk'; function makeCandidate(overrides: Partial = {}): DeadCodeCandidate { @@ -24,6 +25,24 @@ function makeMetadata(overrides: Partial = {}): DeadCo }; } +describe('escapeTableCell', () => { + it('should escape pipe characters', () => { + expect(escapeTableCell('a|b|c')).toBe('a\\|b\\|c'); + }); + + it('should replace newlines with spaces', () => { + expect(escapeTableCell('line1\nline2')).toBe('line1 line2'); + }); + + it('should handle both pipes and newlines', () => { + expect(escapeTableCell('a|b\nc|d')).toBe('a\\|b c\\|d'); + }); + + it('should return unchanged string when no special characters', () => { + expect(escapeTableCell('normalText')).toBe('normalText'); + }); +}); + describe('filterByIgnorePatterns', () => { it('should return all candidates when no patterns provided', () => { const candidates = [makeCandidate(), makeCandidate({ file: 'src/helpers.ts' })]; @@ -57,6 +76,17 @@ describe('filterByIgnorePatterns', () => { const result = filterByIgnorePatterns(candidates, ['**/generated/**']); expect(result).toHaveLength(1); }); + + 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('formatPrComment', () => { @@ -90,6 +120,26 @@ describe('formatPrComment', () => { expect(comment).toContain('class'); }); + 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); + + for (const type of types) { + expect(comment).toContain(`| ${type} |`); + } + }); + + it('should escape pipe characters in candidate names', () => { + const candidates = [makeCandidate({ name: 'fn|with|pipes' })]; + const comment = formatPrComment(candidates); + + expect(comment).toContain('fn\\|with\\|pipes'); + expect(comment).not.toContain('`fn|with|pipes`'); + }); + 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 }) @@ -143,4 +193,11 @@ describe('formatPrComment', () => { const comment = formatPrComment(candidates); expect(comment).toContain('Powered by [Supermodel]'); }); + + it('should render table header with all columns', () => { + const candidates = [makeCandidate()]; + const comment = formatPrComment(candidates); + + expect(comment).toContain('| Name | Type | File | Line | Confidence |'); + }); }); diff --git a/src/__tests__/integration.test.ts b/src/__tests__/integration.test.ts index 7859691..67951f0 100644 --- a/src/__tests__/integration.test.ts +++ b/src/__tests__/integration.test.ts @@ -80,24 +80,21 @@ describe.skipIf(SKIP_INTEGRATION)('Integration Tests', () => { expect(typeof result.metadata.analysisMethod).toBe('string'); }); - it('should detect dead code in this repo', () => { - // We intentionally have uncalled functions in dead-code.ts and markdown.ts - const candidates = result.deadCodeCandidates; - + 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: ${candidates.length}`); + console.log(`Dead code candidates: ${result.deadCodeCandidates.length}`); console.log(`Alive code: ${result.metadata.aliveCode}`); console.log(`Analysis method: ${result.metadata.analysisMethod}`); - for (const dc of candidates) { + for (const dc of result.deadCodeCandidates) { console.log(` [${dc.confidence}] ${dc.type} ${dc.name} @ ${dc.file}:${dc.line} — ${dc.reason}`); } - expect(candidates.length).toBeGreaterThan(0); + expect(result.metadata.totalDeclarations).toBeGreaterThan(0); }); - it('should include file, name, line, type, confidence, and reason on every candidate', () => { + it('should include valid fields on every candidate', () => { for (const dc of result.deadCodeCandidates) { expect(dc.file).toBeTruthy(); expect(dc.name).toBeTruthy(); @@ -108,27 +105,15 @@ describe.skipIf(SKIP_INTEGRATION)('Integration Tests', () => { } }); - it('should find known dead functions by name', () => { - const names = result.deadCodeCandidates.map(c => c.name); - - // These exist in src/dead-code.ts and src/markdown.ts but are never called - const knownDead = ['truncateString', 'groupByDirectory', 'fileSeverity', - 'badge', 'barChart', 'numberedList']; - const found = knownDead.filter(n => names.includes(n)); - - console.log(`\nKnown dead functions found: ${found.join(', ')}`); - console.log(`Known dead functions missed: ${knownDead.filter(n => !names.includes(n)).join(', ') || 'none'}`); - - // At least some of our intentionally dead code should be detected - expect(found.length).toBeGreaterThanOrEqual(3); - }); - - it('should respect ignore-patterns filtering', () => { + it('should support client-side ignore-patterns filtering', () => { const all = result.deadCodeCandidates; - const filtered = filterByIgnorePatterns(all, ['**/markdown.ts']); + // Even if no dead code exists, verify the filter runs without error + const filtered = filterByIgnorePatterns(all, ['**/nonexistent/**']); + expect(filtered).toHaveLength(all.length); - expect(filtered.length).toBeLessThan(all.length); - expect(filtered.every(c => c.file !== 'src/markdown.ts')).toBe(true); + // Filtering with a broad pattern should return fewer or equal results + const aggressive = filterByIgnorePatterns(all, ['**/*.ts']); + expect(aggressive.length).toBeLessThanOrEqual(all.length); }); }); diff --git a/src/dead-code.ts b/src/dead-code.ts index 67773c0..d93c964 100644 --- a/src/dead-code.ts +++ b/src/dead-code.ts @@ -4,40 +4,6 @@ import { escapeTableCell } from './markdown'; export type { DeadCodeCandidate, DeadCodeAnalysisResponse, DeadCodeAnalysisMetadata }; -/** - * Truncates a string to the given max length, appending an ellipsis if needed. - */ -export function truncateString(str: string, maxLen: number): string { - if (str.length <= maxLen) return str; - return str.slice(0, maxLen - 1) + '\u2026'; -} - -/** - * Groups candidates by their containing directory. - */ -export function groupByDirectory(candidates: DeadCodeCandidate[]): Map { - const groups = new Map(); - for (const c of candidates) { - const dir = c.file.includes('/') ? c.file.slice(0, c.file.lastIndexOf('/')) : '.'; - const existing = groups.get(dir); - if (existing) { - existing.push(c); - } else { - groups.set(dir, [c]); - } - } - return groups; -} - -/** - * Returns a severity label based on how many dead code items are in a single file. - */ -export function fileSeverity(count: number): 'clean' | 'warning' | 'critical' { - if (count === 0) return 'clean'; - if (count <= 3) return 'warning'; - return 'critical'; -} - /** * Filters dead code candidates by user-provided ignore patterns. * The API handles all analysis server-side; this is purely for diff --git a/src/markdown.ts b/src/markdown.ts index f34a4c2..7c6e871 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -1,42 +1,6 @@ -/** - * Markdown formatting utilities for PR comments. - */ - -/** - * Wraps text in a collapsible details/summary block. - */ -export function collapsible(summary: string, body: string): string { - return `
${summary}\n\n${body}\n\n
`; -} - -/** - * Creates a markdown badge image link. - */ -export function badge(label: string, value: string, color: string): string { - const encodedLabel = encodeURIComponent(label); - const encodedValue = encodeURIComponent(value); - return `![${label}](https://img.shields.io/badge/${encodedLabel}-${encodedValue}-${color})`; -} - -/** - * Renders a horizontal bar chart using unicode block characters. - */ -export function barChart(value: number, max: number, width = 20): string { - const filled = Math.round((value / max) * width); - const empty = width - filled; - return '\u2588'.repeat(filled) + '\u2591'.repeat(empty); -} - /** * Escapes pipe characters for safe rendering inside markdown tables. */ export function escapeTableCell(text: string): string { return text.replace(/\|/g, '\\|').replace(/\n/g, ' '); } - -/** - * Converts a flat list into a markdown numbered list. - */ -export function numberedList(items: string[]): string { - return items.map((item, i) => `${i + 1}. ${item}`).join('\n'); -}