diff --git a/packages/deslop-js/src/constants.ts b/packages/deslop-js/src/constants.ts index 463a9ce7d0..b9ddf80537 100644 --- a/packages/deslop-js/src/constants.ts +++ b/packages/deslop-js/src/constants.ts @@ -377,12 +377,8 @@ export const INLINE_TYPE_PREVIEW_KEYS = 4; export const SIMPLIFIABLE_EXPRESSION_MEMBER_ACCESS_DEPTH = 6; -export const ANALYSIS_ERROR_PRINT_LIMIT = 20; - export const DUPLICATE_INLINE_TYPE_HIGH_MEMBER_COUNT = 5; -export const SEMANTIC_PROGRAM_BUDGET_MS = 30_000; - export const SEMANTIC_TRACE_MAX_ENTRIES = 5; export const DEFAULT_DUPLICATE_BLOCK_MIN_TOKENS = 50; diff --git a/packages/deslop-js/src/report/cycles.ts b/packages/deslop-js/src/report/cycles.ts index a725cb9ed8..c70e9db10f 100644 --- a/packages/deslop-js/src/report/cycles.ts +++ b/packages/deslop-js/src/report/cycles.ts @@ -4,21 +4,7 @@ import { MAX_TOTAL_CYCLES, MAX_SCC_SIZE_FOR_ENUMERATION, } from "../constants.js"; - -const UNDEFINED_INDEX = -1; - -interface TarjanState { - indexCounter: number; - indices: number[]; - lowlinks: number[]; - onStack: boolean[]; - stack: number[]; -} - -interface DfsFrame { - node: number; - successorPosition: number; -} +import { findStronglyConnectedComponents } from "../utils/find-strongly-connected-components.js"; // A value-form import (`import { Props } from "./barrel"`) whose every // symbol resolves to a type-only export (interface / type alias) in the @@ -100,92 +86,6 @@ const cycleHasModuleInitAccess = (cycle: number[], initAccessEdges: Set) return false; }; -const findStronglyConnectedComponents = (adjacencyList: number[][]): number[][] => { - const nodeCount = adjacencyList.length; - if (nodeCount === 0) { - return []; - } - - const state: TarjanState = { - indexCounter: 0, - indices: Array(nodeCount).fill(UNDEFINED_INDEX), - lowlinks: Array(nodeCount).fill(0), - onStack: Array(nodeCount).fill(false), - stack: [], - }; - - const components: number[][] = []; - const dfsStack: DfsFrame[] = []; - - for (let startNode = 0; startNode < nodeCount; startNode++) { - if (state.indices[startNode] !== UNDEFINED_INDEX) { - continue; - } - - state.indices[startNode] = state.indexCounter; - state.lowlinks[startNode] = state.indexCounter; - state.indexCounter++; - state.onStack[startNode] = true; - state.stack.push(startNode); - - dfsStack.push({ node: startNode, successorPosition: 0 }); - - while (dfsStack.length > 0) { - const frame = dfsStack[dfsStack.length - 1]; - const successors = adjacencyList[frame.node]; - - if (frame.successorPosition < successors.length) { - const successor = successors[frame.successorPosition]; - frame.successorPosition++; - - if (state.indices[successor] === UNDEFINED_INDEX) { - state.indices[successor] = state.indexCounter; - state.lowlinks[successor] = state.indexCounter; - state.indexCounter++; - state.onStack[successor] = true; - state.stack.push(successor); - - dfsStack.push({ node: successor, successorPosition: 0 }); - } else if (state.onStack[successor]) { - state.lowlinks[frame.node] = Math.min( - state.lowlinks[frame.node], - state.indices[successor], - ); - } - } else { - const currentNode = frame.node; - const currentLowlink = state.lowlinks[currentNode]; - const currentIndex = state.indices[currentNode]; - dfsStack.pop(); - - if (dfsStack.length > 0) { - const parentFrame = dfsStack[dfsStack.length - 1]; - state.lowlinks[parentFrame.node] = Math.min( - state.lowlinks[parentFrame.node], - currentLowlink, - ); - } - - if (currentLowlink === currentIndex) { - const component: number[] = []; - let poppedNode: number; - do { - poppedNode = state.stack.pop()!; - state.onStack[poppedNode] = false; - component.push(poppedNode); - } while (poppedNode !== currentNode); - - if (component.length >= 2) { - components.push(component); - } - } - } - } - } - - return components; -}; - const canonicalizeCycle = (cycle: number[], graph: DependencyGraph): number[] => { if (cycle.length === 0) { return []; @@ -270,7 +170,9 @@ const enumerateElementaryCycles = ( export const detectCycles = (graph: DependencyGraph): CircularDependency[] => { const adjacencyList = buildAdjacencyList(graph); const initAccessEdges = buildModuleInitAccessEdgeSet(graph); - const components = findStronglyConnectedComponents(adjacencyList); + const components = findStronglyConnectedComponents(adjacencyList).filter( + (component) => component.length >= 2, + ); const allCycles: number[][] = []; const seenKeys = new Set(); diff --git a/packages/deslop-js/src/report/re-export-cycles.ts b/packages/deslop-js/src/report/re-export-cycles.ts index 879b8a0013..6eb5bf0e94 100644 --- a/packages/deslop-js/src/report/re-export-cycles.ts +++ b/packages/deslop-js/src/report/re-export-cycles.ts @@ -1,4 +1,5 @@ import type { DependencyGraph, ReExportCycle } from "../types.js"; +import { findStronglyConnectedComponents } from "../utils/find-strongly-connected-components.js"; /** * Reports cycles in the subgraph of `isReExportEdge` edges only. These are @@ -22,7 +23,7 @@ export const detectReExportCycles = (graph: DependencyGraph): ReExportCycle[] => adjacency[edge.source].push(edge.target); } - const sccComponents = computeStronglyConnectedComponents(adjacency); + const sccComponents = findStronglyConnectedComponents(adjacency); const findings: ReExportCycle[] = []; for (const component of sccComponents) { @@ -56,74 +57,3 @@ export const detectReExportCycles = (graph: DependencyGraph): ReExportCycle[] => ); return findings; }; - -/** - * Iterative Tarjan's SCC. Singleton components are returned too so the - * caller can distinguish a real self-loop from a node with no edges. - */ -const computeStronglyConnectedComponents = (adjacency: number[][]): number[][] => { - const nodeCount = adjacency.length; - if (nodeCount === 0) return []; - - const indices: number[] = new Array(nodeCount).fill(-1); - const lowLinks: number[] = new Array(nodeCount).fill(0); - const onStack: boolean[] = new Array(nodeCount).fill(false); - const tarjanStack: number[] = []; - const components: number[][] = []; - let nextIndex = 0; - - for (let startNode = 0; startNode < nodeCount; startNode++) { - if (indices[startNode] !== -1) continue; - - const dfsStack: { node: number; successorPosition: number }[] = [ - { node: startNode, successorPosition: 0 }, - ]; - indices[startNode] = nextIndex; - lowLinks[startNode] = nextIndex; - nextIndex++; - onStack[startNode] = true; - tarjanStack.push(startNode); - - while (dfsStack.length > 0) { - const frame = dfsStack[dfsStack.length - 1]; - const successors = adjacency[frame.node]; - - if (frame.successorPosition < successors.length) { - const successorNode = successors[frame.successorPosition]; - frame.successorPosition++; - if (indices[successorNode] === -1) { - indices[successorNode] = nextIndex; - lowLinks[successorNode] = nextIndex; - nextIndex++; - onStack[successorNode] = true; - tarjanStack.push(successorNode); - dfsStack.push({ node: successorNode, successorPosition: 0 }); - } else if (onStack[successorNode]) { - if (indices[successorNode] < lowLinks[frame.node]) { - lowLinks[frame.node] = indices[successorNode]; - } - } - } else { - if (lowLinks[frame.node] === indices[frame.node]) { - const component: number[] = []; - let popped: number; - do { - popped = tarjanStack.pop()!; - onStack[popped] = false; - component.push(popped); - } while (popped !== frame.node); - components.push(component); - } - dfsStack.pop(); - if (dfsStack.length > 0) { - const parent = dfsStack[dfsStack.length - 1]; - if (lowLinks[frame.node] < lowLinks[parent.node]) { - lowLinks[parent.node] = lowLinks[frame.node]; - } - } - } - } - } - - return components; -}; diff --git a/packages/deslop-js/src/utils/oxc-ast-node.ts b/packages/deslop-js/src/utils/oxc-ast-node.ts index 3eb7a1b8fb..b54dcdcdae 100644 --- a/packages/deslop-js/src/utils/oxc-ast-node.ts +++ b/packages/deslop-js/src/utils/oxc-ast-node.ts @@ -13,21 +13,6 @@ export const getNodeStringField = (node: OxcAstNode, key: string): string | unde return typeof value === "string" ? value : undefined; }; -export const getNodeChild = (node: OxcAstNode, key: string): OxcAstNode | undefined => { - const value = node[key]; - return isOxcAstNode(value) ? value : undefined; -}; - -export const getNodeChildArray = (node: OxcAstNode, key: string): OxcAstNode[] => { - const value = node[key]; - if (!Array.isArray(value)) return []; - const children: OxcAstNode[] = []; - for (const candidate of value) { - if (isOxcAstNode(candidate)) children.push(candidate); - } - return children; -}; - export const getIdentifierName = (node: unknown): string | undefined => { if (!isOxcAstNode(node)) return undefined; if (node.type !== "Identifier") return undefined;