lat.md 0.10.1 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import { existsSync } from 'node:fs';
3
3
  import { basename, dirname, extname, join, relative } from 'node:path';
4
4
  import { listLatticeFiles, loadAllSections, extractRefs, flattenSections, parseFrontmatter, parseSections, buildFileIndex, resolveRef, } from '../lattice.js';
5
5
  import { scanCodeRefs } from '../code-refs.js';
6
- import { SOURCE_EXTENSIONS } from '../source-parser.js';
6
+ import { SOURCE_EXTENSIONS, clearSymbolCache } from '../source-parser.js';
7
7
  import { walkEntries } from '../walk.js';
8
8
  import { INIT_VERSION, readInitVersion } from '../init-version.js';
9
9
  function filePart(id) {
@@ -82,6 +82,7 @@ async function tryResolveSourceRef(target, projectRoot) {
82
82
  }
83
83
  }
84
84
  export async function checkMd(latticeDir) {
85
+ clearSymbolCache();
85
86
  const projectRoot = dirname(latticeDir);
86
87
  const files = await listLatticeFiles(latticeDir);
87
88
  const allSections = await loadAllSections(latticeDir);
@@ -10,6 +10,8 @@ export type SourceSymbol = {
10
10
  /** All source file extensions that lat can parse (derived from grammarMap). */
11
11
  export declare const SOURCE_EXTENSIONS: ReadonlySet<string>;
12
12
  export declare function parseSourceSymbols(filePath: string, content: string): Promise<SourceSymbol[]>;
13
+ /** Clear the symbol cache. Call between top-level operations. */
14
+ export declare function clearSymbolCache(): void;
13
15
  /**
14
16
  * Check whether a source file path (relative to projectRoot) has a given symbol.
15
17
  * Used by lat check to validate source code wiki links lazily.
@@ -477,8 +477,17 @@ function extractGoSymbols(tree) {
477
477
  * Handles plain identifiers and pointer declarators (*name).
478
478
  */
479
479
  function cFuncName(declarator) {
480
- if (declarator.type === 'function_declarator') {
481
- const inner = declarator.childForFieldName('declarator');
480
+ // Unwrap pointer_declarator layers (for functions returning pointers,
481
+ // e.g. `JSRuntime *JS_NewRuntime(void)` → pointer_declarator > function_declarator)
482
+ let node = declarator;
483
+ while (node.type === 'pointer_declarator') {
484
+ const child = node.childForFieldName('declarator');
485
+ if (!child)
486
+ return null;
487
+ node = child;
488
+ }
489
+ if (node.type === 'function_declarator') {
490
+ const inner = node.childForFieldName('declarator');
482
491
  if (!inner)
483
492
  return null;
484
493
  if (inner.type === 'identifier')
@@ -510,6 +519,13 @@ function cVarName(declarator) {
510
519
  return null;
511
520
  node = inner;
512
521
  }
522
+ // Unwrap array_declarator (e.g. `char js_version[]`)
523
+ if (node.type === 'array_declarator') {
524
+ const inner = node.childForFieldName('declarator');
525
+ if (!inner)
526
+ return null;
527
+ node = inner;
528
+ }
513
529
  if (node.type === 'identifier')
514
530
  return node.text;
515
531
  if (node.type === 'pointer_declarator') {
@@ -530,8 +546,13 @@ function extractCSymbols(tree) {
530
546
  return symbols;
531
547
  }
532
548
  /**
533
- * Walk C AST nodes, collecting symbols. Recurses into preproc_ifdef /
534
- * preproc_ifndef blocks so header include guards don't hide declarations.
549
+ * Walk C AST nodes, collecting symbols. Recurses into preprocessor
550
+ * conditional blocks (ifdef/ifndef/if), linkage specifications
551
+ * (extern "C" { ... }), and declaration lists so that include guards
552
+ * and conditional compilation don't hide declarations.
553
+ *
554
+ * For #if/#ifdef/#ifndef, only the "then" branch is traversed —
555
+ * preproc_else and preproc_elif children are skipped.
535
556
  */
536
557
  function collectCNodes(parent, symbols) {
537
558
  for (let i = 0; i < parent.childCount; i++) {
@@ -576,7 +597,12 @@ function collectCNodes(parent, symbols) {
576
597
  }
577
598
  }
578
599
  else if (node.type === 'type_definition') {
579
- const declarator = node.childForFieldName('declarator');
600
+ let declarator = node.childForFieldName('declarator');
601
+ // Unwrap pointer_declarator for pointer typedefs
602
+ // e.g. `typedef struct __JSValue *JSValue;`
603
+ while (declarator?.type === 'pointer_declarator') {
604
+ declarator = declarator.childForFieldName('declarator') ?? null;
605
+ }
580
606
  const name = declarator?.type === 'type_identifier' ? declarator.text : null;
581
607
  if (name) {
582
608
  symbols.push({
@@ -590,18 +616,33 @@ function collectCNodes(parent, symbols) {
590
616
  }
591
617
  else if (node.type === 'declaration') {
592
618
  const declarator = node.childForFieldName('declarator');
593
- const name = declarator ? cVarName(declarator) : null;
594
- if (name) {
619
+ // Try as function declaration first (e.g. `void greet(const char *name);`
620
+ // in headers), then fall back to variable.
621
+ const funcName = declarator ? cFuncName(declarator) : null;
622
+ if (funcName) {
595
623
  symbols.push({
596
- name,
597
- kind: 'variable',
624
+ name: funcName,
625
+ kind: 'function',
598
626
  startLine,
599
627
  endLine,
600
628
  signature: firstLine(node.text),
601
629
  });
602
630
  }
631
+ else {
632
+ const name = declarator ? cVarName(declarator) : null;
633
+ if (name) {
634
+ symbols.push({
635
+ name,
636
+ kind: 'variable',
637
+ startLine,
638
+ endLine,
639
+ signature: firstLine(node.text),
640
+ });
641
+ }
642
+ }
603
643
  }
604
- else if (node.type === 'preproc_def') {
644
+ else if (node.type === 'preproc_def' ||
645
+ node.type === 'preproc_function_def') {
605
646
  const name = extractName(node);
606
647
  if (name) {
607
648
  symbols.push({
@@ -614,10 +655,21 @@ function collectCNodes(parent, symbols) {
614
655
  }
615
656
  }
616
657
  else if (node.type === 'preproc_ifdef' ||
617
- node.type === 'preproc_ifndef') {
618
- // Recurse into include guard / conditional blocks
658
+ node.type === 'preproc_ifndef' ||
659
+ node.type === 'preproc_if') {
660
+ // Recurse into conditional blocks (then-branch only).
661
+ // preproc_else / preproc_elif children are skipped.
662
+ collectCNodes(node, symbols);
663
+ }
664
+ else if (node.type === 'linkage_specification' ||
665
+ node.type === 'declaration_list') {
666
+ // extern "C" { ... } wraps declarations in linkage_specification
667
+ // containing a declaration_list — recurse through both.
619
668
  collectCNodes(node, symbols);
620
669
  }
670
+ else if (node.type === 'preproc_else' || node.type === 'preproc_elif') {
671
+ // Skip else/elif branches of preprocessor conditionals.
672
+ }
621
673
  }
622
674
  }
623
675
  function firstLine(text) {
@@ -634,19 +686,32 @@ export async function parseSourceSymbols(filePath, content) {
634
686
  const tree = p.parse(content);
635
687
  if (!tree)
636
688
  return [];
637
- if (ext === '.py') {
638
- return extractPySymbols(tree);
639
- }
640
- if (ext === '.rs') {
641
- return extractRustSymbols(tree);
642
- }
643
- if (ext === '.go') {
644
- return extractGoSymbols(tree);
689
+ try {
690
+ if (ext === '.py') {
691
+ return extractPySymbols(tree);
692
+ }
693
+ if (ext === '.rs') {
694
+ return extractRustSymbols(tree);
695
+ }
696
+ if (ext === '.go') {
697
+ return extractGoSymbols(tree);
698
+ }
699
+ if (ext === '.c' || ext === '.h') {
700
+ return extractCSymbols(tree);
701
+ }
702
+ return extractTsSymbols(tree);
645
703
  }
646
- if (ext === '.c' || ext === '.h') {
647
- return extractCSymbols(tree);
704
+ finally {
705
+ tree.delete();
648
706
  }
649
- return extractTsSymbols(tree);
707
+ }
708
+ // Per-invocation cache for parsed source symbols, keyed by absolute file path.
709
+ // Prevents re-parsing the same file when multiple wiki links reference it
710
+ // (e.g. 20+ links to quickjs.c would otherwise parse a 60K-line file 20 times).
711
+ const symbolCache = new Map();
712
+ /** Clear the symbol cache. Call between top-level operations. */
713
+ export function clearSymbolCache() {
714
+ symbolCache.clear();
650
715
  }
651
716
  /**
652
717
  * Check whether a source file path (relative to projectRoot) has a given symbol.
@@ -654,24 +719,33 @@ export async function parseSourceSymbols(filePath, content) {
654
719
  */
655
720
  export async function resolveSourceSymbol(filePath, symbolPath, projectRoot) {
656
721
  const absPath = join(projectRoot, filePath);
657
- let content;
658
- try {
659
- content = readFileSync(absPath, 'utf-8');
722
+ let cached = symbolCache.get(absPath);
723
+ if (!cached) {
724
+ let content;
725
+ try {
726
+ content = readFileSync(absPath, 'utf-8');
727
+ }
728
+ catch {
729
+ cached = { symbols: [] };
730
+ symbolCache.set(absPath, cached);
731
+ return { found: false, symbols: [] };
732
+ }
733
+ try {
734
+ const symbols = await parseSourceSymbols(filePath, content);
735
+ cached = { symbols };
736
+ }
737
+ catch (err) {
738
+ cached = {
739
+ symbols: [],
740
+ error: `failed to parse "${filePath}": ${err instanceof Error ? err.message : String(err)}`,
741
+ };
742
+ }
743
+ symbolCache.set(absPath, cached);
660
744
  }
661
- catch {
662
- return { found: false, symbols: [] };
663
- }
664
- let symbols;
665
- try {
666
- symbols = await parseSourceSymbols(filePath, content);
667
- }
668
- catch (err) {
669
- return {
670
- found: false,
671
- symbols: [],
672
- error: `failed to parse "${filePath}": ${err instanceof Error ? err.message : String(err)}`,
673
- };
745
+ if (cached.error) {
746
+ return { found: false, symbols: cached.symbols, error: cached.error };
674
747
  }
748
+ const { symbols } = cached;
675
749
  const parts = symbolPath.split('#');
676
750
  if (parts.length === 1) {
677
751
  // Simple symbol: getConfigDir
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lat.md",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "A knowledge graph for your codebase, written in markdown",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.30.2",