Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions packages/deslop-js/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
106 changes: 4 additions & 102 deletions packages/deslop-js/src/report/cycles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -100,92 +86,6 @@ const cycleHasModuleInitAccess = (cycle: number[], initAccessEdges: Set<string>)
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 [];
Expand Down Expand Up @@ -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<string>();

Expand Down
74 changes: 2 additions & 72 deletions packages/deslop-js/src/report/re-export-cycles.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
};
15 changes: 0 additions & 15 deletions packages/deslop-js/src/utils/oxc-ast-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading