Frontend Development 29 min read

Code Impact Range Analysis and Dependency‑Tree Construction for Large Frontend Projects

This article explains how to analyse the impact range of code changes in large frontend projects, describes the architecture and core techniques—including project dependency‑tree building, AST parsing, GitLab diff extraction, and change propagation—provides concrete JavaScript/TypeScript examples, and discusses visualization of the results.

大转转FE
大转转FE
大转转FE
Code Impact Range Analysis and Dependency‑Tree Construction for Large Frontend Projects

1. Introduction

1.1 What is Code Impact Range

In large frontend projects, as the codebase grows and development cycles lengthen, the impact of code changes becomes increasingly complex. Code‑impact‑range analysis evaluates how modifications to files, functions, or variables propagate through the project, especially when a function is altered, affecting callers and dependent modules.

Simply put, the impact range refers to the set of files added, modified, or deleted and the chain reactions caused when those functions or variables are referenced elsewhere. By traversing the project's dependency tree, developers can track and manage the spread of these impacts.

1.2 Background and Value

In real‑world team development, a change can affect many files, functions, or modules. Failing to identify the propagation scope may cause problems such as:

🛠️ Public component changes not fully verified, leading to feature anomalies;

🚫 Public method changes causing logical errors across multiple modules.

Impact‑range analysis reduces the risk of bugs introduced by changes, enables precise code reviews, and improves development efficiency. It helps teams to:

🔍 Conduct accurate code reviews by identifying the propagation scope, reducing blind spots;

🧪 Optimise testing by selecting test cases based on the change impact, cutting redundant tests;

📈 Assess project complexity and raise code quality.

1.3 Difference Between Impact Range and Code Coverage

Metric

Code Impact Range

Code Coverage

Focus

Propagation impact of changed code

Scope of tested code

Purpose

Assess change risk, optimise tests

Evaluate test completeness

Computation

Dependency analysis, AST parsing

Execution of code paths

Advantages

1️⃣ Accurately identify affected areas 2️⃣ Optimise test case selection 3️⃣ Help evaluate project complexity

1️⃣ Ensure all code paths are exercised 2️⃣ Provide a visual metric of code quality

Disadvantages

1️⃣ Dependency analysis can be costly 2️⃣ May miss indirect impacts

1️⃣ Focuses only on coverage, not on change propagation 2️⃣ May not reflect test effectiveness or hidden bugs

Application Scenarios

1️⃣ Frequent code changes – ensure no hidden issues 2️⃣ Multi‑person collaboration – identify cross‑module impact

1️⃣ Guarantee each code path is tested at least once 2️⃣ When coverage is low, use it to guide test quality improvement

Code‑impact‑range analysis focuses on identifying routes, modules, files, or components affected by a change, tracking propagation to ensure no undetected problems.

Code‑coverage analysis concentrates on the extent of test coverage, ensuring every code path is exercised to avoid missed errors.

2. Architecture Process Design

The impact‑range architecture consists of several key steps that systematically describe how a project analyses and marks the influence of code changes.

Main modules include:

🌳 Project dependency‑tree construction: parse dependencies and build a complete call chain;

📄 Code‑change analysis: extract changed functions and variables from GitLab diff data;

🎯 Impact‑range marking: trace call paths of changed functions to determine affected files/modules;

📉 Result visualisation: display analysis results for code review and testing.

These modules work together to give development teams precise insight into change impact, improve code quality, and lower risk.

Below is a schematic diagram (image omitted).

3. Core Technical Implementation

The following sections present essential logic snippets that guide developers in building their own tools.

3.1 Project Dependency‑Tree Construction

a. Determine Project Type

First, identify whether the project is Umi, Vue, or React based on configuration files or package.json dependencies.

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

/**
 * Determine if the project is a Umi project
 * - Parse `zz.config.ts` for `tplType` or check `package.json` for an umi dependency
 */
async function getIsUmiProject({ appName, commitId }) {
  const zzConfigCode = await getFileContent({ appName, srcPath: "zz.config.ts", commitId });
  if (zzConfigCode) {
    const ast = parser.parse(zzConfigCode, { sourceType: "module", plugins: ["typescript", "jsx"] });
    let tplType = null;
    traverse(ast, {
      ObjectProperty(path) {
        if (path.node.key.name === "tplType") tplType = path.node.value.value;
      }
    });
    if (tplType === "umi") return true;
  }
  const packageJsonContent = await getFileContent({ appName, srcPath: "package.json", commitId });
  if (packageJsonContent) {
    const packageJson = JSON.parse(packageJsonContent);
    const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
    return Object.keys(dependencies).some(dep => dep.includes("@umijs") || dep === "umi");
  }
  return false;
}

/**
 * Determine if the project is Vue or React
 */
async function getIsVueOrReact({ appName, commitId }) {
  const packageJsonContent = await getFileContent({ appName, srcPath: "package.json", commitId });
  if (packageJsonContent) {
    const packageJson = JSON.parse(packageJsonContent);
    const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
    if (dependencies["vue"]) return "vue";
    if (dependencies["react"]) return "react";
  }
}

b. Parse Project Routing Information

Depending on the project type, the router directory differs (Umi → `config`, Vue → `src/router`, React → `src/router`). The algorithm recursively reads route files, resolves absolute paths, and records module titles.

async function getRouters({ appName, commitId }) {
  const arr = [];
  const projectType = await getIsVueOrReact({ appName, commitId });
  const isUmi = await getIsUmiProject({ appName, commitId });
  // Determine router directory
  let routerPath = isUmi ? "config" : "src/router";
  // Recursively collect all routes
  await loopRouter({ appName, arr, srcPath: routerPath, commitId, isUmi });
  // Add main entry files
  const mainEntry = projectType === "vue"
    ? ["src/main.js", "src/main.ts"]
    : ["src/app.jsx", "src/App.jsx", "src/App.tsx", "src/app.tsx"];
  for (const filePath of mainEntry) {
    if (await getFileContent({ appName, commitId, srcPath: filePath })) {
      arr.push({ filePath, modulePath: "/", moduleTitle: "Project entry file" });
      break;
    }
  }
  return arr;
}

c. AST Parsing to Build the Dependency Tree (Babel AST)

Parsing import/export statements from JavaScript/TypeScript/Vue files provides a clear view of file‑level dependencies.

const babelParser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generate = require("@babel/generator").default;
const types = require("@babel/types");
const getFullPath = require("../utils/getFullPath");
const fs = require("fs");
/**
 * Get import and export information of a file
 */
async function getImportsAndExports({ appName, filePath, code = "", commitId, lang }) {
  lang = lang || (filePath.includes("ts") ? "ts" : "js");
  const imports = [], exports = [];
  const ast = babelParser.parse(code, {
    filename: lang === "ts" ? "anyName.ts" : "anyName.tsx",
    sourceType: "module",
    plugins: ["typescript", "decorators-legacy", lang === "ts" ? "jsx" : "jsx"]
  });
  traverse(ast, {
    ImportDeclaration(path) {
      const importObj = { path: path.node.source.value };
      const specifiers = path.node.specifiers;
      if (specifiers.length === 1 && types.isImportDefaultSpecifier(specifiers[0])) {
        importObj.type = "default";
        importObj.value = [specifiers[0].local.name];
      } else if (specifiers.length > 1 || types.isImportSpecifier(specifiers[0])) {
        importObj.type = "named";
        importObj.value = specifiers.map(s => s.local.name);
      }
      imports.push(importObj);
    },
    ExportNamedDeclaration(path) {
      if (path.node.specifiers.length) {
        path.node.specifiers.forEach(spec => exports.push({ name: spec.exported.name, type: "named", path: filePath, code: generate(spec).code }));
      }
      if (path.node.declaration && types.isVariableDeclaration(path.node.declaration)) {
        path.node.declaration.declarations.forEach(decl => exports.push({ name: decl.id.name, type: "named", path: filePath, code: generate(decl).code }));
      }
      if (path.node.source) {
        imports.push({ path: path.node.source.value, type: "default", code: ["default"] });
      }
    },
    ExportDefaultDeclaration(path) {
      exports.push({ name: "default", type: "default", path: filePath, code: generate(path.node.declaration).code });
    }
  });
  // Resolve import paths to absolute paths
  for (let imp of imports) {
    if (imp.path?.length > 1) {
      imp.path = await getFullPath({ appName, usePath: filePath, importOrExportPath: imp.path, commitId });
    }
  }
  const filteredImports = imports.filter(item => item.path && fs.existsSync(item.path) && !item.path.includes("src/router/") && !item.path.includes("config/routes"));
  return { imports: filteredImports, exports };
}

3. Impact File Marking and Dependency‑Tree Output

a. Apply Change Data to Tree Nodes

function processChanges(node, changes = []) {
  const change = changes.find(c => c.filePath === node.filePath);
  if (change) {
    node.diff = change.diff;
    node.diffResults = { functions: [], variables: [] };
    if (change.diffResults?.functions?.length) node.diffResults.functions.push(...change.diffResults.functions);
    if (change.diffResults?.variables?.length) node.diffResults.variables.push(...change.diffResults.variables.map(v => ({ name: v.name })));
    node.updateStatus = change.updateStatus;
  }
  node.children?.forEach(child => processChanges(child, changes));
}

b. Propagate Impact to Parent Nodes

function propagateChanges(node, path = []) {
  path.push(node);
  if (node.diffResults) {
    const { functions = [], variables = [] } = node.diffResults;
    const affectedNames = new Set([
      ...functions.map(f => f.name),
      ...variables.map(v => v.name)
    ]);
    for (let i = path.length - 2; i >= 0; i--) {
      const parent = path[i];
      if (parent.imports && parent.imports.length) {
        const allImportedFuncs = parent.imports.flatMap(ref => (ref && ref.type === "named" ? ref.value : []));
        const hasAffectedImport = allImportedFuncs.some(funcName => affectedNames.has(funcName));
        if (hasAffectedImport) {
          parent.updateStatus = parent.updateStatus || "affected";
          if (!parent.diffResults) parent.diffResults = { functions: [] };
          if (parent.importsReferenceMap && parent.importsReferenceMap.length) {
            parent.importsReferenceMap.forEach(ref => {
              if (affectedNames.has(ref.importedFunc)) {
                ref.originFuncs.forEach(originFunc => {
                  if (!parent.diffResults.functions.some(f => f.name === originFunc)) {
                    parent.diffResults.functions.push({ name: originFunc });
                  }
                });
              }
            });
          }
        }
      }
    }
  }
  if (node.children) node.children.forEach(child => propagateChanges(child, path));
  path.pop();
}

c. Patch Diff Data to the Whole Tree

exports.patchDiffToTree = (tree, changes) => {
  try {
    tree.forEach(node => processChanges(node, changes));
    tree.forEach(node => propagateChanges(node));
    return tree;
  } catch (error) {
    console.error("Error in patchDiffToTree:", error);
    return tree;
  }
};

4. Impact Visualisation

The final visualisation uses the react-org-tree component (wrapped for the project) to display nodes marked as added, modified, deleted, or affected, giving developers an intuitive view of how a change spreads across routes, components, and files.

Below is a screenshot of the visualised dependency tree (image omitted).

5. Conclusion

This article demonstrates how to build a code‑impact‑range analysis tool, covering dependency‑tree construction, AST‑based parsing, change extraction from GitLab diff, impact propagation, and front‑end visualisation. While only core logic is shown, a production‑grade system must handle many framework‑specific edge cases, dynamic imports, and complex call‑graph scenarios.

Key take‑aways for building a similar tool include:

Understand framework differences (React vs. Vue) in dependency resolution and state management;

Leverage Babel AST to extract imports, exports, functions, and variables;

Compare change sets between branches to detect added, modified, or removed symbols;

Propagate impact through the dependency graph to highlight indirect effects;

Provide a clear visual interface for developers to assess change risk.

Further work would involve handling dynamic routing, code‑splitting, and advanced patterns such as hooks, HOCs, mixins, and provide/inject mechanisms.

For more practical insights from the company, follow the official public account.

frontendASTcode analysisdependency treeGitLab diffimpact range
大转转FE
Written by

大转转FE

Regularly sharing the team's thoughts and insights on frontend development

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.