semrel-extra/toposource

View on GitHub
src/main/ts/toposource.ts

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
import {
  TAnalyze,
  TAnalyzeOptions,
  TDepMap,
  TEdges,
  TGraph
} from './interface'

export const analyze: TAnalyze = (edges: TEdges, opts: TAnalyzeOptions = {graphs: true, queue: true, check: true, strict: true}) => {
  const _edges = opts.strict ? [...edges].sort() : edges
  const { next, prev } = getHops(_edges)
  opts.check && checkLoop(next)

  const sources = getSources(_edges)
  const graphs = opts.graphs ? getGraphs(sources, next) : undefined
  const queue = opts.queue ? getQueue(sources, next, prev) : undefined

  return {
    next,
    prev,
    sources,
    queue,
    graphs
  } as ReturnType<TAnalyze>
}

const getHops = (edges: TEdges): {next: TDepMap, prev: TDepMap} => {
  const next = new Map<string, string[]>()
  const prev = new Map<string, string[]>()
  const pushHop = (deps: TDepMap, a: string, b: string) => {
    if (deps.has(a)) {
      (deps.get(a) as string[]).push(b)
    } else {
      deps.set(a, [b])
    }
  }

  for (const [from, to] of edges) {
    if (to) {
      pushHop(next, from, to)
      pushHop(prev, to, from)
    }
  }
  return { next, prev }
}

const getSources = (edges: TEdges): string[] => [...new Set(edges.map(([from]) => from))]
  .filter(node => edges.every(([,to]) => to !== node))

const getQueue = (sources: string[], next: TDepMap, prev: TDepMap) => {
  const nodes = new Set()
  const todo = [...sources]
  const batch: Set<string> = new Set()

  for (const [i, node] of todo.entries()) {
    if ((prev.get(node) || []).every((p) => nodes.has(p))) {
      nodes.add(node)
      pushToSet(batch, ...(next.get(node) || []))

    } else {
      pushToSet(batch, node)
    }

    if (i === todo.length - 1) {
      todo.push(...batch.values())
      batch.clear()
    }
  }

  return [...nodes.values()]
}

const getGraphs = (sources: string[], next: TDepMap) => {
  const graphs: TGraph[] = []

  for (const source of sources) {
    const nodes = mergeNested(new Set([source]), next)
    const values = [...nodes.values()]
    const same = graphs.find(_graph => [..._graph.nodes.values()].some(node => values.includes(node)))

    if (same) {
      same.sources.push(source)
      pushToSet(same.nodes, ...values)
      continue
    }

    graphs.push({nodes, sources: [source]})
  }

  return graphs
}

export const checkLoop = (next: TDepMap): void => {
  for (const [node, children] of next) {
    const desc = mergeNested(new Set(children), next)
    if (desc.has(node)) {
      throw new Error(`Loop detected: ${[node,...desc.values()]}`)
    }
  }
}

const pushToSet = (set: Set<string>, ...items: string[]): Set<string> => {
  for (const item of items) {
    set.add(item)
  }
  return set
}

const mergeNested = (nodes: Set<string>, deps: TDepMap): Set<string> => {
  for (const node of nodes) {
    pushToSet(nodes, ...(deps.get(node) || []))
  }
  return nodes
}