VS کوڈ کی زبان API کا استعمال کرتے ہوئے TypeScript میں کوڈ گراف کیسے بنائیں

جدید کوڈ بیسز پر تشریف لانا مشکل ہوتا جا رہا ہے۔ ضروری نہیں کہ یہ سچ ہو کیونکہ ڈویلپر خود زیادہ کوڈ لکھ رہے ہیں۔ اکثر ایسا ہوتا ہے کیونکہ کوڈنگ اسسٹنٹ نے کوڈ کی سیکڑوں یا ہزاروں لائنیں تیار کی ہیں اور مسئلہ کوڈ کا جائزہ بن گیا ہے۔

ایل ایل ایم سے پہلے کے دور میں، کوڈ کی چند سطریں لکھنے میں دن لگ سکتے تھے۔ اس کا مطلب یہ ہے کہ آپ کی شراکت کی وجہ سے ہر پروجیکٹ کے لیے آپ کی صورتحال بتدریج بڑھتی ہے۔ جب آپ کسی نئی ٹیم میں شامل ہوں یا کوئی نیا کام شروع کریں تو آپ کو صرف اس کا جائزہ لینے اور نئے کوڈ سے واقف ہونے کی ضرورت ہے۔

لیکن آج، ایک ہی پرامپٹ منٹوں میں سینکڑوں فائلوں میں کوڈ کی ہزاروں لائنیں تیار کر سکتا ہے۔ اس پیمانے پر، روایتی کوڈ ریویو فارمیٹس ٹوٹنا شروع ہو جاتے ہیں اور آپ کو کوڈ کا جائزہ لینے میں اصل میں اسے لکھنے سے زیادہ وقت صرف کرتے ہیں۔

مثال کے طور پر، آپ کے پاس ایک بڑا TypeScript پروجیکٹ ہو سکتا ہے اور آپ بظاہر آسان سوالات کا جواب دینا چاہتے ہیں جیسے:

"اس خصوصیت کو کیا کہتے ہیں؟”

آپ شاید فائلوں کو تلاش کرکے شروع کریں گے۔ آپ ایڈیٹر کی "فائنڈ ریفرینسز” فیچر بھی استعمال کر سکتے ہیں۔ آپ تعریفوں کے درمیان بھی منتقل ہو سکتے ہیں۔ آپ درآمد، برآمد، اور فنکشن کے نام تلاش کر سکتے ہیں۔

لیکن مسئلہ کے بارے میں سوچنے کا ایک اور طریقہ ہے۔ اپنے کوڈ بیس کو فائلوں کے مجموعے کے طور پر سمجھنے کے بجائے، آپ اسے گراف کے طور پر ماڈل بنا سکتے ہیں۔

فنکشن نوڈس بن جاتے ہیں اور کالز کنارے بن جاتے ہیں۔

جب آپ گراف کوڈ بناتے ہیں، تو آپ اس طرح ہوتے ہیں، "میں اس فنکشن کو کیا کہوں؟” ایک ہی سوال آتا ہے۔ یا "یہ فنکشن آخر کال کیا کرتا ہے؟” یہ گراف ٹراورسل مسئلہ بن جاتا ہے۔

اس ٹیوٹوریل میں، ہم TypeScript اور VS Code کے بلٹ ان لینگویج API کا استعمال کرتے ہوئے کوڈ گراف کا بنیادی حصہ بناتے ہیں۔ ہم اپنا TypeScript پارسر نہیں لکھیں گے۔ اس کے بجائے، یہ VS کوڈ اور انسٹال لینگویج ایکسٹینشن کے ذریعے پہلے سے فراہم کردہ معنوی معلومات کا استعمال کرتا ہے۔ نتیجہ ایک گراف ہے جس میں فائلیں، فنکشنز، طریقے، اور کال تعلقات ہیں جو VS Code Webview میں دکھائے جا سکتے ہیں۔

انڈیکس

ہم کیا بنا رہے ہیں

ہم VS Code’s Call Hierarchy API کو ایک چھوٹا کوڈ گراف انجن بنانے کے لیے استعمال کریں گے جو افعال اور طریقوں کے درمیان تعلقات کو دریافت کرتا ہے اور پھر ان تعلقات کو متعدد ہاپس پر دریافت کرتا ہے۔ راستے میں، ہم ہم آہنگی، پرانے زبان کے ٹول کے حوالہ جات، کیشنگ، اور فالتو ٹراورسلز کو سنبھالیں گے تاکہ یہ یقینی بنایا جا سکے کہ گراف مستحکم اور موثر رہے۔

شرطیں

پیروی کرنے سے پہلے، آپ کو درج ذیل سے واقف ہونا چاہئے:

  • ٹائپ اسکرپٹ اور بنیادی غیر مطابقت پذیر پروگرامنگ async/await

  • تمام وی ایس کوڈ ہم آہنگ کوڈ، وی ایس کوڈ ایکسٹینشن API، اور vscode.commands.executeCommand

  • بنیادی گراف کے تصورات جیسے کہ نوڈس، کنارے، اور چوڑائی-پہلی تلاش (BFS)

  • TypeScript میں نقشوں، صفوں اور عام افعال کے ساتھ کام کرنا

چلو!

ہم کہتے ہیں کہ آپ کے پاس اس طرح کا کوڈ ہے:

function checkout() {
  processPayment();
}

function processPayment() {
  chargeCard();
}

function chargeCard() {
  saveTransaction();
}

function saveTransaction() {
  // persist transaction
}

ہم سورس کوڈ کو گراف میں تبدیل کرنا چاہتے ہیں۔ ایک حقیقی کوڈبیس میں، گراف متعدد فائلوں کو پھیلا سکتا ہے۔

تصویر یہ دکھاتی ہے کہ جب ایک گراف کے طور پر تصور کیا جاتا ہے تو ملٹی فائل کوڈبیس کیسا لگتا ہے۔

نفاذ کے دو اہم حصے ہیں: ایکسٹینشن ہوسٹ گراف کو بازیافت کرنے کے لیے VS کوڈ کی زبان API کا استعمال کرتا ہے۔ نتیجے میں گراف ویب ویو میں ظاہر ہوتا ہے۔ اس فن تعمیر کا ایک دلچسپ حصہ گراف بلڈر ہے۔

1. VS کوڈ زبان API کو سمجھیں۔

VS کوڈ پہلے ہی کئی کمانڈز کو بے نقاب کرتا ہے جو کہ ایکسٹینشنز لینگویج انٹیلی جنس کو استفسار کرنے کے لیے استعمال کر سکتی ہیں۔ اس منصوبے کے لیے چار چیزیں خاص طور پر مفید ہیں:

حکم مقصد
vscode.executeDocumentSymbolProvider دستاویزات میں علامتیں تلاش کریں۔
vscode.prepareCallHierarchy کرنسی لیئر آئٹمز کے لیے مقام کا تعین کریں۔
vscode.provideIncomingCalls کالر تلاش کریں۔
vscode.provideOutgoingCalls ایک وصول کنندہ تلاش کریں۔

یہ APIs زبان کے مخصوص نفاذ کے سب سے اوپر بیٹھے ہیں۔ TypeScript اور JavaScript کے لیے، TypeScript زبان کی خدمت بنیادی معلومات فراہم کرتی ہے۔ دیگر زبانیں، جیسے Gopls for Go، Rust-analyser for Rust، اور Pyright یا Pylance for Python، اپنی زبان کی توسیع اور زبان کے سرورز کے ذریعے اسی طرح کی فعالیت فراہم کرتی ہیں۔

یہ خاص طور پر اہم ہے کیونکہ یہ ہر زبان کے لیے الگ الگ پارسر اور کال گراف انجن بنانے کی ضرورت کو ختم کرتا ہے۔ اگر لینگویج ایکسٹینشن VS کوڈ کے ذریعے دستاویز کی علامت اور کال لیئر سپورٹ فراہم کرتی ہے، تو وہی گراف بلڈنگ آرکیٹیکچر اس معلومات کو براہ راست استعمال کر سکتا ہے۔

AST آپ کو بتا سکتا ہے کہ ایک فنکشن کالنگ ایکسپریشن پر مشتمل ہے۔ درآمدات، فائلیں، ماڈیولز، کلاسز، عرفی نام، اور دیگر زبان کی تعمیرات خود بخود آپ کو یہ نہیں بتاتی ہیں کہ کال کے حوالے سے کیا فنکشن ہے۔

لینگویج سرور پہلے سے ہی زیادہ تر سیمنٹک کام انجام دیتا ہے۔ لہذا، ایک اور تجزیہ کار اور علامت حل کرنے کے بجائے، آپ ایسی معلومات طلب کر سکتے ہیں جو VS کوڈ کو پہلے سے معلوم ہے۔

2. توسیع کی ترتیبات

ہمارے package.json درج ذیل کمانڈ کا اعلان کریں:

{
  "main": "./out/extension.js",
  "engines": {
    "vscode": "^1.85.0"
  },
  "activationEvents": [],
  "contributes": {
    "commands": [
      {
        "command": "codeGraphView.open",
        "title": "Code Graph: Open Graph for Active File",
        "icon": "$(type-hierarchy)"
      }
    ],
    "menus": {
      "editor/title": [
        {
          "command": "codeGraphView.open",
          "group": "navigation",
          "when": "resourceLangId == typescript"
        }
      ]
    }
  },
  "dependencies": {
    "elkjs": "^0.9.3"
  }
}

ایکسٹینشن ہوسٹ اور ویب ویو مختلف ماحول میں چلتے ہیں اور اس لیے الگ الگ بنڈل ہوتے ہیں۔ ایک آسان esbuild ترتیب اس طرح نظر آئے گی:

const extensionConfig = {
  entryPoints: ['src/extension.ts'],
  bundle: true,
  outfile: 'out/extension.js',
  external: ['vscode'],
  format: 'cjs',
  platform: 'node',
};

const webviewConfig = {
  entryPoints: ['webview/main.ts'],
  bundle: true,
  outfile: 'out/webview/main.js',
  format: 'iife',
  platform: 'browser',
};

ایکسٹینشن کوڈ نوڈ میں چلتا ہے اور ویب ویو کوڈ براؤزر کے ماحول میں چلتا ہے۔

3. گراف ڈیٹا ماڈل ڈیزائن

زبان API کو کال کرنے سے پہلے، آپ کو یہ فیصلہ کرنا ہوگا کہ گراف کیسا نظر آئے گا۔ کچھ مفید ماڈلز میں شامل ہیں:

export interface SymbolRow {
  id: string;
  name: string;
  kind: 'function' | 'method';
  line: number;
  character: number;
}

export interface FileNode {
  id: string;
  label: string;
  file: string;
  symbols: SymbolRow[];
}

export interface CallEdge {
  id: string;
  source: string;
  target: string;
}

export interface GraphData {
  rootFileId: string;
  rootSymbolId?: string;
  roots: string[];
  files: FileNode[];
  edges: CallEdge[];
  truncated: boolean;
}

دو اہم تصورات ہیں:

  1. کوئی راستہ نہیں FileNode فائل سے تعلق رکھنے والے افعال یا طریقوں پر مشتمل ہے۔

  2. کوئی راستہ نہیں CallEdge دو علامتوں کے درمیان تعلق کی نشاندہی کرتا ہے۔

کنارے کی واقفیت کو مستقل رکھیں۔

caller → callee

پھر checkout() فون کال processPayment()گراف میں ہمیشہ شامل ہوتا ہے:

checkout → processPayment

یہاں تک کہ اگر آپ کو آنے والی کال کی درخواست کرتے وقت وہ رشتہ دریافت ہوتا ہے۔

مستحکم علامت ID

فنکشن کے نام منفرد نہیں ہیں۔ منصوبوں میں آسانی سے شامل ہوسکتا ہے:

// users.ts
function save() {}

اور:

// payments.ts
function save() {}

لہذا، ہمیں علامت کے مقام کی بنیاد پر ایک شناخت کنندہ کی ضرورت ہے۔

function idOf(
  uri: vscode.Uri,
  pos: vscode.Position
): string {
  return `${uri.toString()}#${pos.line}:${pos.character}`;
}

کال پرت اندراجات کے لیے، ان کا استعمال کریں۔ selectionRange:

function itemId(
  item: vscode.CallHierarchyItem
): string {
  return idOf(
    item.uri,
    item.selectionRange.start
  );
}

استعمال کریں selectionRange یہ مفید ہے کیونکہ یہ اعلان کے پورے جسم یا دائرہ کار کے بجائے علامت کے نام کی شناخت کرتا ہے۔ یہ مستحکم ID ڈپلیکیشن کی بنیاد ہے۔ اگر ایک ہی فنکشن گراف کے ذریعے متعدد راستوں پر پایا جاتا ہے، تو آپ دیکھ سکتے ہیں کہ تمام دریافتیں ایک ہی نوڈ کا حوالہ دے رہی ہیں۔

4. افعال اور طریقے تلاش کریں۔

گراف بنانے کا پہلا مرحلہ فعال فائل میں علامت تلاش کرنا ہے۔ VS کوڈ دستاویز کی علامتوں کو اس کے ذریعے ظاہر کرتا ہے:

vscode.executeDocumentSymbolProvider

ہم اسے یہ کہہ سکتے ہیں:

/*
 * Get all symbols in a document using VS Code's
 * built-in language service instead of parsing the code ourselves. This was we can get the methods, functions, variables within a document
 */
async function getDocumentSymbols(
  uri: vscode.Uri
): Promise {

  /*
   * `vscode.executeDocumentSymbolProvider` delegates the analysis
   * to the language provider registered for the document's language.
   */

  const result =
    await vscode.commands.executeCommand<
      vscode.DocumentSymbol[] | undefined
    >(
      'vscode.executeDocumentSymbolProvider',
      uri
    );

  // Return an empty list if no symbols are found.
  return result ?? [];
}

لوٹی ہوئی علامتیں ایک درجہ بندی بناتی ہیں۔ مثال کے طور پر:

اپنے گراف کو بصری طور پر آسان بنانے کے لیے، آپ کو ایک درجہ بندی بنانے کی ضرورت ہے۔

ہمیں اس درجہ بندی سے گزرنے اور ایسی علامتیں جمع کرنے کی ضرورت ہے جو قابل کال کوڈ کی نمائندگی کر سکیں۔

// Check whether a symbol can be treated as a callable node.
const isCallableKind = (
  kind: vscode.SymbolKind,
  includeConstructors: boolean
) =>
  kind === vscode.SymbolKind.Function ||
  kind === vscode.SymbolKind.Method ||
  (
    includeConstructors &&
    kind === vscode.SymbolKind.Constructor
  );

علامتی درختوں کی بار بار جانچ کی جا سکتی ہے۔

  // Recursively collect functions, methods, and variables from a symbol tree
function collectCandidates(
  symbols: vscode.DocumentSymbol[],
  isCallable: (
    kind: vscode.SymbolKind
  ) => boolean,
  out: vscode.DocumentSymbol[] = []
) {
  for (const symbol of symbols) {
    if (
      isCallable(symbol.kind) ||
      symbol.kind === vscode.SymbolKind.Variable
    ) {
      out.push(symbol);
    } else if (
      symbol.children.length
    ) {
      collectCandidates(
        symbol.children,
        isCallable,
        out
      );
    }
  }
  return out;
}

یہ متغیرات پر غور کرنے کے قابل ہے کیونکہ ان کو تفویض کردہ فنکشنز زبان کے آلے کے لحاظ سے مختلف طریقے سے رپورٹ کیے جا سکتے ہیں۔ مثال کے طور پر:

const handler = () => {
  // ...
};

علامتوں کو متغیر کے طور پر رپورٹ کیا جا سکتا ہے چاہے وہ کالنگ کے درجہ بندی میں شریک ہوں۔

5. کسی مخصوص مقام پر علامت تلاش کریں۔

جب کوئی صارف کسی خاص طریقہ کے لیے گراف کھولتا ہے، تو ہمیں یہ تعین کرنے کی ضرورت ہوتی ہے کہ کون سی علامت کرسر کی پوزیشن پر مشتمل ہے۔ چونکہ دستاویز کی علامتیں درجہ بندی کے مطابق ہوتی ہیں، اس لیے ہم مقام پر مشتمل گہری ترین علامت کو بار بار تلاش کر سکتے ہیں۔

// Find the most specific symbol containing a given position.
function symbolAt(
  symbols: vscode.DocumentSymbol[],
  position: vscode.Position
) {
  for (const symbol of symbols) {
    if (symbol.range.contains(position)) {
      return (
        symbolAt(
          symbol.children,
          position
        ) ?? symbol
      );
    }
  }
  return undefined;
}

یہ ایڈیٹر اور گراف کے درمیان ایک پل فراہم کرتا ہے۔ صارف سورس فائل میں جگہ کا انتخاب کرتا ہے۔ علامت کے ساتھ مقام کی تصدیق کریں۔ اس کے بعد یہ ان علامتوں کو کال لیئر آئٹمز سے تعبیر کرتا ہے۔

6. کرنسی کے درجہ بندی کو حل کریں۔

کال لیئر API دو مراحل میں کام کرتا ہے: پہلا:

position → CallHierarchyItem

پھر:

CallHierarchyItem → incoming/outgoing calls

آپ اپنی اشیاء کو مندرجہ ذیل طریقے سے تیار کر سکتے ہیں:

async function prepare(
  uri: vscode.Uri,
  position: vscode.Position
) {
  // VS Code's language tooling already knows how to resolve
  // symbols in a source file.
  // So we can use it to prepare a call hierarchy for the symbol at this location.
  const items =
    await vscode.commands.executeCommand<
      vscode.CallHierarchyItem[] | undefined
    >(
      'vscode.prepareCallHierarchy',
      uri,
      position
    );

  // The command returns an array of hierarchy items. In our case,
  // we are interested in the symbol directly under the cursor,
  // so we use the first result.
  // Optional chaining also handles the case where no symbol
  // could be resolved at the given position.
  return items?.[0];
}

ایک بار جب آپ کے پاس آئٹم ہو جائے، تو آپ کال کرنے والے سے پوچھ سکتے ہیں:

async function callers(
  item: vscode.CallHierarchyItem
) {
  // Ask VS Code for all symbols that call this item.
  const calls =
    await vscode.commands.executeCommand<
      vscode.CallHierarchyIncomingCall[] | undefined
    >(
      'vscode.provideIncomingCalls',
      item
    );

  // Return the calling symbols, defaulting to an empty list when none are found.
  return (
    calls ?? []
  ).map(call => call.from);
}

یا وصول کنندہ:

async function callees(
  item: vscode.CallHierarchyItem
) {
  // Ask VS Code for all symbols called by this item.
  const calls =
    await vscode.commands.executeCommand<
      vscode.CallHierarchyOutgoingCall[] | undefined
    >(
      'vscode.provideOutgoingCalls',
      item
    );

  // Return the called symbols, defaulting to an empty list when none are found.
  return (
    calls ?? []
  ).map(call => call.to);
}

درج ذیل صورتوں میں:

ایک کوڈ گراف میں اکثر متعدد کال کرنے والے ہو سکتے ہیں۔

آنے والی کال کی درخواست کریں۔ processPaymentزبان API فراہم کرتا ہے:

checkout
retryPayment

پھر ہم اس نتیجے کو حسب ذیل معمول بناتے ہیں:

checkout → processPayment
retryPayment → processPayment

لہذا، ایک ہی گراف ڈھانچہ آنے والے اور جانے والے دونوں ٹراورسلز کی نمائندگی کرسکتا ہے۔

7. علامتی رجسٹری بنانا

جیسا کہ آپ گراف کو کرال کرتے ہیں، آپ کو ایک ہی علامت بار بار نظر آ سکتی ہے۔ غور کریں:

رجسٹری کے ذریعے بہتر گراف ویژولائزیشن ڈیپلیکیشن کا نتیجہ کم شور اور صاف ستھرا لائنوں میں ہوتا ہے۔

ہمیں ایک نوڈ بنانے کی ضرورت ہے۔Cتین نہیں۔ رجسٹری وہ ڈپلیکیشن پرت فراہم کرتی ہے۔

class Registry {
  // Keep files and their symbols separately so they can be reused across the graph.
  private readonly files =
    new Map();

  // Store symbols by ID for fast lookup and duplicate detection.
  private readonly rows =
    new Map();

  get size() {
    return this.rows.size;
  }

  has(id: string) {
    return this.rows.has(id);
  }

  register(
    uri: vscode.Uri,
    name: string,
    kind: vscode.SymbolKind,
    position: vscode.Position
  ): string {
    // Generate a stable ID from the file and symbol position.
    const id =
      idOf(uri, position);

    // Avoid registering the same symbol more than once.
    if (this.rows.has(id)) {
      return id;
    }

    const fileId =
      uri.toString();

    let file =
      this.files.get(fileId);

    // Create the file entry the first time we encounter it.
    if (!file) {
      file = {
        id: fileId,
        label:
          vscode.workspace
            .asRelativePath(uri),
        file: uri.fsPath,
        symbols: [],
      };

      this.files.set(
        fileId,
        file
      );
    }

    // Normalize VS Code's symbol kind into the graph's simpler representation.
    const row: SymbolRow = {
      id,
      name,
      kind:
        kind ===
        vscode.SymbolKind.Method
          ? 'method'
          : 'function',
      line: position.line,
      character:
        position.character,
    };

    // Store the symbol globally and under its containing file.
    this.rows.set(id, row);
    file.symbols.push(row);

    return id;
  }
}Now the graph builder can repeatedly register symbols without worrying about duplicates.

8. BFS کے ساتھ گراف کو عبور کرنا

ایک سنگل کال لیئر تلاش ایک ہاپ فراہم کرتی ہے۔ ایک مفید کوڈ گراف کے لیے متعدد ہاپس کی ضرورت ہوتی ہے۔ سے دیکھا گیا۔ A() آپ ہمیں یہ بھی بتا سکتے ہیں کہ یہ کال کرتا ہے۔ B()لیکن یہ ہمیں کسی بھی چیز کے بارے میں کچھ نہیں بتاتا ہے۔ B() اگلی بار کال کریں۔

ایک مفید گراف بنانے کے لیے، ہم تکراری طور پر درج ذیل رشتوں کی پیروی کرتے ہیں: A → B → C → D. چونکہ ہر تلاش گراف کو ایک مختلف سطح تک بڑھاتا ہے، اس لیے متعدد ہاپس کو موثر طریقے سے عبور کرنے کے لیے ایک ٹراورسل حکمت عملی جیسے BFS کی ضرورت ہوتی ہے۔

مثال کے طور پر:

// Assuming a graph with 4 hops
A --> B --> C --> D --> E

اگر ہم شروع کرتے ہیں A اور یہ 3 (3 hops) کی گہرائی مانگتا ہے۔ ہم چاہتے ہیں:

Depth 0: A
Depth 1: B
Depth 2: C
Depth 3: D

چوڑائی-پہلی تلاش (BFS) ایک قدرتی فٹ ہے کیونکہ گراف واضح طور پر ہاپ کی گہرائی کے ارد گرد بنایا گیا ہے۔ سرکٹ چوکسی برقرار رکھتا ہے۔

current frontier
      ↓
discover neighbors
      ↓
next frontier
      ↓
discover neighbors

پہلے سے طے شدہ نفاذ ہے:

const walk = async (
  start: Handle,
  direction: 'incoming' | 'outgoing',
  limit: number
) => {
  // Traverse the call graph one level at a time, starting from the given symbol.
  let frontier: Handle[] = [start];

  for (
    let depth = 0;
    depth < limit &&
    frontier.length > 0;
    depth++
  ) {
    // Resolve the next level in parallel, limiting concurrency to six lookups.
    const results =
      await mapLimit(
        frontier,
        6,
        handle =>
          oneHop(
            handle,
            direction
          )
      );

    const next: Handle[] = [];

    frontier.forEach(
      (handle, index) => {
        for (
          const other
            of results[index]
        ) {
          // Register newly discovered symbols before adding their relationships.
          if (
            !registry.has(
              other.node.id
            )
          ) {
            registry.register(
              other.node.uri,
              other.node.name,
              other.node.kind,
              other.node.pos
            );
          }

          // Preserve the direction of the call relationship in the graph.
          if (
            direction === 'outgoing'
          ) {
            addEdge(
              handle.node.id,
              other.node.id
            );
          } else {
            addEdge(
              other.node.id,
              handle.node.id
            );
          }

          next.push(other);
        }
      }
    );

    // Continue the traversal from the symbols discovered at this depth.
    frontier = next;
  }
};

کہ mapLimit مددگار ہم آہنگی زبان کے سرور کی درخواستوں کی تعداد کو کنٹرول کرتا ہے۔

async function mapLimit(
  items: T[],
  limit: number,
  fn: (item: T) => Promise
): Promise {
  // Run at most `limit` async operations at the same time.
  const results =
    new Array(items.length);

  let next = 0;

  // Create workers that share the next available item.
  const workers =
    Array.from(
      {
        length:
          Math.min(
            limit,
            items.length
          ),
      },
      async () => {
        while (
          next < items.length
        ) {
          const index = next++;

          results[index] =
            await fn(
              items[index]
            );
        }
      }
    );
  await Promise.all(workers);
  return results;
}

اہم فرق یہ ہے کہ BFS صرف ایک مقامی حساب ہے، جبکہ علامتوں کو حل کرنے کے لیے اکثر VS کوڈ کے لینگویج ٹولز سے اصل کام کرنے کی ضرورت ہوتی ہے۔

ہر ایک علامت کے لیے جو ہم دیکھتے ہیں، کوڈ گراف ویو کو انکمنگ یا آؤٹ گوئنگ کالز کے لیے لینگویج سروس سے استفسار کرنے کی ضرورت پڑ سکتی ہے۔ ان سوالات میں سورس فائلوں کو پارس کرنا، علامتوں کو حل کرنا، اور لینگویج سرورز کے ساتھ بات چیت شامل ہو سکتی ہے۔ جیسا کہ گراف بڑھتا ہے، ان درخواستوں کی تعداد میں بھی اضافہ ہوتا ہے۔

لہذا اگرچہ ٹراورسل بذات خود آسان ہے، اس تلاش کو ترتیب وار کرنا مجموعی عمل کو بہت سست بنا سکتا ہے۔ mapLimit ہم اس مسئلے کو حل کرتے ہیں متعدد آزاد زبان کے آلے کی درخواستوں کو بیک وقت عمل کرنے کی اجازت دے کر، جب کہ زبان کی خدمت پر بھاری پڑنے سے بچنے کے لیے ہم آہنگی پر حدیں لگاتے ہیں۔

9. ہینڈلنگ سائیکل

اصل کوڈ درخت نہیں ہے۔ یہ ایک گراف ہے۔ اس کا مطلب ہے کہ آپ کا سائیکل نارمل ہے۔

مثال کے طور پر:

چونکہ کوڈبیس درخت کے بجائے ایک گراف ہے، اس لیے سرکلر کالیں کثرت سے ہوتی ہیں۔

ایک بولی بار بار چلنے والا سفر غیر معینہ مدت تک جاری رہ سکتا ہے۔ لہذا ہمیں اس بات کا سراغ لگانے کی ضرورت ہے جو ہم نے پہلے ہی دریافت کیا ہے۔ لیکن ٹھیک ٹھیک تفصیلات ہیں. سادہ:

Set

یہ ہمیشہ کافی نہیں ہوتا ہے اگر ایک ہی نوڈ کو مختلف گہرائیوں سے پہنچایا جا سکے۔ اس کے بجائے، ایک نوڈ کو دریافت کرتے وقت، آپ ذخیرہ کر سکتے ہیں کہ کتنی گہرائی باقی ہے۔

const explored =
  new Map();

پھر:

// Track the deepest remaining traversal already performed for this node.
const key =
  `${direction}:${node.id}`;
if (
  (explored.get(key) ?? -1)
  < remainingDepth
) {
  // Revisit only when this traversal can explore deeper than before.
  explored.set(
    key,
    remainingDepth
  );
  next.push(node);
}

اس کا مطلب یہ ہے کہ اگر آپ پہلے ایک نوڈ پر پہنچ گئے جس میں 1 ہاپ باقی ہے، لیکن بعد میں ایک نوڈ دریافت کریں جس میں 3 ہاپس باقی ہیں، تو آپ اس نوڈ کو دوبارہ دریافت کر سکتے ہیں۔ یہ نوڈ کو "ملاحظہ شدہ" کے طور پر علاج کرنے سے زیادہ درست ہے۔

10. گراف کی حدود کی وضاحت کریں۔

گراف بہت تیزی سے بڑھ سکتے ہیں۔ انتہائی منسلک فنکشنز میں درجنوں کال کرنے والے ہو سکتے ہیں۔ ان بھیجنے والوں میں سے ہر ایک کے درجنوں مرسل ہو سکتے ہیں۔ اس وجہ سے، گراف بنانے والوں پر واضح پابندیاں ہونی چاہئیں۔

مثال کے طور پر:

const MAX_SYMBOLS = 400;
const MAX_CALLS_PER_SYMBOL = 50;
const HOP_CONCURRENCY = 6;

جب گراف اپنی حد تک پہنچ جاتا ہے، تو آپ یہ دکھاوا نہیں کرنا چاہتے کہ گراف مکمل ہے۔ اس کے بجائے:

let truncated = false;

اور:

if (
  registry.size >=
  MAX_SYMBOLS
) {
  truncated = true;
  continue;
}

نتیجہ GraphData پھر آپ اپنے UI کو بتا سکتے ہیں:

This graph was truncated.

یہ ایک غیر متوقع طور پر بڑے کوڈبیس کو توسیع دینے کی اجازت دینے سے بہتر ہے۔

11. فائل فلٹرنگ

لینگویج سرور ان فائلوں سے تعلقات واپس کر سکتا ہے جو اس ایپلیکیشن کا حصہ نہیں ہیں جس کی ہم تلاش کر رہے ہیں۔ یہ ایک بلڈ فائل یا آؤٹ پٹ ہو سکتا ہے جو انحصار انسٹال یا زبان کے ساتھ مخصوص بلڈ/رن ٹائم ٹاسک کے ذریعے تیار کیا جاتا ہے۔ مثال کے طور پر، ایک TypeScript پروجیکٹ کا نتیجہ ہو سکتا ہے:

node_modules

ایک ازگر پروجیکٹ کا نتیجہ ہو سکتا ہے:

site-packages

آپ ان راستوں کو گراف میں شامل کرنے سے پہلے فلٹر کر سکتے ہیں۔

const DEPENDENCY_DIRS =
  //(node_modules|vendor|target|.venv|venv|site-packages|__pycache__|build|obj|.dart_tool)//;

function isWorkspaceFile(
  uri: vscode.Uri
): boolean {
  if (
    uri.scheme !== 'file' ||
    DEPENDENCY_DIRS.test(uri.path)
  ) {
    return false;
  }
  return !!vscode.workspace
    .getWorkspaceFolder(uri);
}

یہ گراف کو صارف کے کام کی جگہ پر مرکوز رکھتا ہے۔ یہ لسانی ذہانت اور اطلاق کے رویے کے درمیان اہم فرق کو بھی ظاہر کرتا ہے۔ لینگویج سرور آپ کو بتاتا ہے کہ یہ کیا حل کر سکتا ہے۔ گراف بلڈر اس بات کا تعین کرتا ہے کہ گراف کا حصہ کیا ہوگا۔

12. کیوں کچھ کنارے خاموشی سے غائب ہو جاتے ہیں؟

بڑے گراف میں، کچھ فنکشنز جو واضح طور پر ایک دوسرے کو کہتے ہیں منقطع ہو سکتے ہیں اور کچھ بھی غلطی کی اطلاع نہیں دے گا۔ مسئلہ یہ ہے۔ CallHierarchyItem زبان کی خدمت کی حیثیت سے منسلک۔ اگر وہ حالت باسی ہو جاتی ہے، تو کال کرنے والے یا کالی کو درخواستیں ایک خالی صف واپس کر سکتی ہیں۔ گراف بلڈر کے نقطہ نظر سے، یہ بالکل ایسا لگتا ہے جیسے کال کرنے والے کے بغیر۔

VS کوڈ کے نفاذ میں، حالیہ درخواستوں کی ایک محدود تعداد کے لیے کال لیئر سیشن کو برقرار رکھا جاتا ہے۔ مزید برآں، کرالرز حرکت میں ایک ساتھ متعدد سوالات کر سکتے ہیں، جس کی وجہ سے گراف کی تلاش کے دوران پرانی اشیاء دستیاب نہیں ہو سکتی ہیں۔ گراف بلڈر اسے تین طریقوں سے سنبھالتا ہے:

1. ریگولر ڈیٹا اسٹور کریں، لائیو آئٹمز نہیں۔

ہر فنکشن کو اس طرح دکھایا جاتا ہے: NodeRef ID، URI، نام، قسم اور مقام پر مشتمل ہے۔ سنگل ہاپ کے نتائج بھی اس طرح محفوظ کیے گئے ہیں: NodeRefS. یہ ضرورت پڑنے پر کال پرت کے اندراجات کو دوبارہ بنانے کے لیے کافی معلومات فراہم کرتا ہے۔

interface NodeRef {
  id: string;
  uri: vscode.Uri;
  name: string;
  kind: vscode.SymbolKind;
  pos: vscode.Position;
}

2. ہر تیار شدہ شے کی عمر کو ٹریک کریں۔

عالمی epoch جب بھی نئی کال لیئر انٹری تیار ہوتی ہے تو کاؤنٹر میں اضافہ ہوتا ہے۔ ہر ہینڈل اس دور کو ریکارڈ کرتا ہے جس میں شے بنائی گئی تھی۔ اگر آئٹم کافی پرانی ہے، تو کرالر محفوظ کردہ آئٹمز سے ایک نیا آئٹم تیار کرتا ہے۔ NodeRef.

3. مشکوک خالی نتائج کے ساتھ دوبارہ کوشش کریں۔

چھوٹا ورژن oneHop یہ مندرجہ ذیل ہے:

const SESSION_WINDOW = 7;

// Refresh stale language-tooling references and retry once if necessary.
for (let attempt = 0; attempt < 2; attempt++) {
  const stale =
    !current.item ||
    epoch - current.epoch > SESSION_WINDOW;

  if (stale) {
    // Re-resolve the symbol before using an expired CallHierarchyItem.
    const fresh = await prepareFresh(
      current.node.uri,
      current.node.pos
    );

    if (!fresh) {
      return [];
    }

    current = fresh;
  }

  const items =
    await lookup(
      current.item!,
      direction
    );

  // A stale reference may return nothing, so invalidate it and retry once.
  if (
    items.length === 0 &&
    attempt === 0 &&
    epoch - current.epoch > SESSION_WINDOW
  ) {
    current = {
      node: current.node,
      epoch: -1,
    };

    continue;
  }

  // Cache the resolved relationships to avoid repeating the language-tooling lookup.
  hopCache.set(key, {
    nodes: items.map(refOf),
    at: Date.now(),
  });

  return items.map(child => ({
    node: refOf(child),
    item: child,
    epoch: current.epoch,
  }));
}

یہاں، lookup ~ کا مخفف ہے۔ vscode.provideIncomingCalls یا vscode.provideOutgoingCalls کمانڈ پہلے بیان کی گئی ہے۔ سیشن کی قطعی حد VS کوڈ میں نفاذ کی ایک تفصیل ہے، نہ کہ ایسی چیز جس پر آپ کی توسیع کو انحصار کرنا چاہیے۔ لہذا، کرالر یہ نہیں سمجھتا کہ کچھ حدود ہمیشہ موجود ہیں۔ کہ SESSION_WINDOW یہ پرانے ہینڈلز کو تروتازہ کرنے کے لیے محض ایک قدامت پسند حد فراہم کرتا ہے۔

یہ گمشدہ کناروں کو کم کرتا ہے لیکن مکمل گراف کی ضمانت نہیں دیتا۔ زبان کے اوزار اب بھی نامکمل معلومات واپس کر سکتے ہیں یا بعض رشتوں کو حل کرنے میں ناکام ہو سکتے ہیں۔

وسیع تر سبق کا اطلاق کال کے درجہ بندی سے آگے لینگویج سروس APIs پر ہوتا ہے۔ یعنی، یہ اس چیز کو ذخیرہ کرتا ہے جو زبان کی خدمت کو آبجیکٹ کو دوبارہ بنانے کے لیے درکار ہوتی ہے، نہ کہ خود آبجیکٹ۔ خالی نتائج کو باسی حالت کے ساتھ کوئی نہیں کے طور پر خود بخود علاج کرنے کے بجائے، انہیں ممکنہ طور پر نامعلوم نتائج کے طور پر سمجھیں۔

13. گراف کو Webview سے جوڑیں۔

گراف کی تعمیر کے بعد، ایکسٹینشن کو گراف کو ظاہر کرنے کے لیے ایک مقام کی ضرورت ہوتی ہے۔ وی ایس کوڈ ویب ویو قدرتی فٹ ہے۔

ایکسٹینشن ہوسٹ ایک پینل بناتا ہے۔

const panel =
  vscode.window.createWebviewPanel(
    'codeGraphView',
    'Code Graph',
    vscode.ViewColumn.Beside,
    {
      enableScripts: true,
      retainContextWhenHidden: true,
    }
  );

گراف کو سیریلائز ڈیٹا کے طور پر ویب ویو کو بھیجا جاتا ہے۔

panel.webview.postMessage({
  command: 'graphData',
  data: graphData,
});

Webview پھر اسے سن سکتا ہے۔

window.addEventListener(
  'message',
  event => {
    const message =
      event.data;

    if (
      message.command !==
      'graphData'
    ) {
      return;
    }

    renderGraph(
      message.data
    );
  }
);

اس وقت مسئلہ کی زبان سرور کی طرف مکمل ہے.

ہم نے درج ذیل تبدیلیاں کی ہیں:

source code

میں:

symbols + relationships

اور پھر:

GraphData

اب ویژولائزیشن پرت اس ڈیٹا کو گراف رینڈر کرنے کے لیے استعمال کر سکتی ہے۔

ELK جیسی لائبریریاں گراف کو بنانے والی منطق کو متاثر کیے بغیر گراف میں پوزیشنوں کا حساب لگانے کی اجازت دیتی ہیں۔

14. گراف بلڈر ٹیسٹ

اس قسم کی ایکسٹینشن کی جانچ کرنا مشکل ہو سکتا ہے جب ہر ٹیسٹ کے لیے چلتے ہوئے VS کوڈ کی مثال اور حقیقی زبان کے سرور کی ضرورت ہو۔ گراف تخلیق کی منطق کو خود VS کوڈ سے الگ کرنا ایک بہتر طریقہ ہے۔ کرالر کو دراصل صرف چند آپریشنز کی ضرورت ہوتی ہے۔

prepareCallHierarchy
provideIncomingCalls
provideOutgoingCalls

آپ اس کمانڈ کا جعلی نفاذ تشکیل دے سکتے ہیں۔ مثال کے طور پر:

const sessions = new Map();

let sessionCounter = 0;

async function executeCommand(
  command,
  ...args
) {
  // Simulate VS Code creating a session when resolving a symbol.
  if (
    command ===
    'vscode.prepareCallHierarchy'
  ) {
    const id =
      'session-' +
      ++sessionCounter;

    sessions.set(id, true);

    return [
      createFakeItem(
        args,
        id
      ),
    ];
  }

  // Simulate call lookups that depend on a still-valid session.
  if (
    command ===
      'vscode.provideIncomingCalls' ||
    command ===
      'vscode.provideOutgoingCalls'
  ) {
    const item = args[0];

    // Return nothing when the CallHierarchyItem belongs to an expired session.
    if (
      !sessions.has(
        item.sessionId
      )
    ) {
      return [];
    }

    return getFakeCalls(
      item
    );
  }
}

موکس ایج کیسز کو ماڈل بنا سکتے ہیں جیسے کہ میعاد ختم ہونے والی کال لیئر سٹیٹ۔ اہم بات یہ ہے کہ اگر جعل ساز سیشن کی مخصوص حد استعمال کرتا ہے تو اس کا مطلب ہے۔ ٹیسٹ ماڈلیہ خود بخود سرکاری VS کوڈ API کی توثیق کے ساتھ نہیں آتا ہے۔

اس کے بعد آپ ایک تعییناتی گراف بنا سکتے ہیں اور کرالر کے آؤٹ پٹ کا ایک سادہ حوالہ BFS سے موازنہ کر سکتے ہیں۔

مثال کے طور پر:

flowchart LR
    A[A] --> B[B]
    A --> C[C]
    B --> D[D]
    C --> D
    D --> E[E]

حوالہ کا نفاذ متوقع کناروں کو جانتا ہے۔ پروڈکشن کرالر جعلی لینگویج سروس کے خلاف چلتا ہے۔ اگر دونوں کے نتائج مختلف ہیں، تو ٹیسٹ ناکام ہو جاتا ہے۔ یہ نقطہ نظر آپ کو ایڈیٹر کے رن ٹائم پر مکمل انحصار کیے بغیر مشکل گراف منطق کی جانچ کرنے کی اجازت دیتا ہے۔

15. کوڈ گراف کی حدود

زبان کے سرور پر مبنی کال گراف مفید ہیں، لیکن وہ پروگرام کے عمل کی مکمل نمائندگی نہیں کرتے ہیں۔ کچھ رشتوں کو جامد کال کے درجہ بندی کے تجزیہ کے ساتھ حل کرنا مشکل یا ناممکن ہو سکتا ہے۔

مثالوں میں شامل ہیں:

غور کریں:

eventEmitter.on(
  'payment.completed',
  handlePayment
);

ڈویلپرز سمجھ سکتے ہیں کہ اس سے واقعات کے درمیان رن ٹائم تعلقات پیدا ہوتے ہیں۔ handlePayment. ایک جامد کال گراف اپنے تعلقات کو باقاعدہ فنکشن کالز کے طور پر ظاہر نہیں کرسکتا ہے۔ لہٰذا گراف کا معیار جزوی طور پر لینگویج سرور پر منحصر ہوتا ہے اور یہ کس قسم کے تعلقات کو حل کر سکتا ہے۔

یہی وجہ ہے کہ گرافس کو مکمل رن ٹائم ماڈلز کے بجائے سیمنٹک قریباً سمجھنا چاہیے۔ زبان کی ایک مخصوص جہت بھی ہے۔

گراف بلڈر بذات خود بڑی حد تک زبان سے متعلق علمی ہے، لیکن مختلف زبان کی توسیعات دستاویز کی علامتوں اور کال کے درجہ بندی کے لیے مختلف سطحوں کی مدد فراہم کر سکتی ہیں۔

نتیجہ

کوڈ گراف بنانے کے لیے آپ کو کمپائلر لکھنے یا کسی پارسر کو شروع سے لاگو کرنے کی ضرورت نہیں ہے۔ VS کوڈ پہلے سے ہی اپنی زبان API کے ذریعے معنوی معلومات کی ایک اہم مقدار کو ظاہر کرتا ہے۔

بنیادی عمل ہیں:

Document -> Document Symbols -> Call Hierarchy -> Graph Nodes + Edges -> BFS Traversal -> GraphData -> Visualization

انجینئرنگ کا سب سے اہم فیصلہ یہ نہیں ہے کہ کیا گراف کیا جائے۔ یہ ایک کارآمد گراف ماڈل منتخب کرنے، مستحکم علامت آئی ڈی تیار کرنے، آنے والی اور جانے والی کالوں کی صحیح ترجمانی کرنے، ٹراورسل ڈیپتھ اور کنکرنسی کو کنٹرول کرنے، سائیکلوں کو ہینڈل کرنے اور لینگویج سرور کی حالت کو بدلنے کے قابل سمجھنے کے بارے میں ہے۔

ایک بار جب یہ ٹکڑے اپنی جگہ پر آجائیں تو تصور ایک الگ مسئلہ بن جاتا ہے۔ یہ علیحدگی فن تعمیر کو ایک واحد VS کوڈ توسیع سے زیادہ مفید بناتی ہے۔

وہی گراف ماڈل آخر کار انحصار کی تلاش، اثرات کے تجزیے، تعمیراتی خیالات، AI سیاق و سباق کے انتخاب، اور تیزی سے پیچیدہ کوڈ بیسز کو نیویگیٹ کرنے کے دیگر طریقوں کی حمایت کر سکتا ہے۔

میں نے اس کی بنیاد پر ایک ورکنگ ورژن بنایا۔ اس تک رسائی حاصل کی جا سکتی ہے https://github.com/otobongfp/code-graph-view۔

میں ان تمام عمدہ چیزوں کو دیکھنے کا منتظر ہوں جو ہم سافٹ ویئر انجینئرنگ کے عمل میں حصہ ڈالنے کے لیے گراف کے ساتھ تخلیق کر سکتے ہیں۔

اوپر تک سکرول کریں۔