Selaa lähdekoodia

fix(extraction): count all file-level-tracked langs (incl .properties) as indexed (#544)

Completes #357. The no-symbol file-level class is yaml/twig/properties, but the
count fix only covered yaml/twig — so a .properties-only project still printed
"No files found to index" even though the files were stored. Introduce a single
isFileLevelOnlyLanguage predicate (the canonical set behind the tree-sitter
no-symbol branch, xml excluded since its MyBatis extractor emits a file node)
and use it at both count sites and the extraction dispatch so the list can't
drift. Adds .properties regression coverage for indexAll() and indexFiles().

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby Mchenry 3 viikkoa sitten
vanhempi
sitoutus
cdbf451440

+ 4 - 0
CHANGELOG.md

@@ -9,6 +9,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
 
+### Fixes
+
+- Indexing a project that contains only config-style files (YAML, Twig, or `.properties`) no longer misleadingly reports "No files found to index" — these files are tracked at the file level and are now counted as indexed. Thanks @luojiyin1987 (#357).
+
 ## [0.9.7] - 2026-05-28
 
 ### New Features

+ 32 - 0
__tests__/extraction.test.ts

@@ -3258,6 +3258,38 @@ export function multiply(a: number, b: number): number {
 
     cg.close();
   });
+
+  it('should count file-level tracked .properties files as indexed', async () => {
+    fs.writeFileSync(path.join(tempDir, 'application.properties'), 'server.port=8080\n');
+    fs.writeFileSync(path.join(tempDir, 'log.properties'), 'log.level=INFO\n');
+
+    const cg = CodeGraph.initSync(tempDir);
+    const result = await cg.indexAll();
+
+    expect(result.success).toBe(true);
+    expect(result.filesIndexed).toBe(2);
+    expect(result.filesSkipped).toBe(0);
+
+    cg.close();
+  });
+
+  it('should count the full file-level tracked class (yaml/twig/properties) in indexFiles()', async () => {
+    fs.writeFileSync(path.join(tempDir, 'app.yaml'), 'name: test\n');
+    fs.writeFileSync(path.join(tempDir, 'view.twig'), '{{ title }}\n');
+    fs.writeFileSync(path.join(tempDir, 'application.properties'), 'server.port=8080\n');
+
+    const cg = CodeGraph.initSync(tempDir);
+    const result = await cg.indexFiles(['app.yaml', 'view.twig', 'application.properties']);
+
+    expect(result.success).toBe(true);
+    expect(result.filesIndexed).toBe(3);
+    expect(result.filesSkipped).toBe(0);
+
+    const tracked = cg.getFiles().map((f) => `${f.path}:${f.language}`).sort();
+    expect(tracked).toEqual(['app.yaml:yaml', 'application.properties:properties', 'view.twig:twig']);
+
+    cg.close();
+  });
 });
 
 describe('Path Normalization', () => {

+ 13 - 0
src/extraction/grammars.ts

@@ -290,6 +290,19 @@ export function isGrammarLoaded(language: Language): boolean {
   return languageCache.has(language);
 }
 
+/**
+ * Languages tracked at the file-record level only: parsing emits zero symbol
+ * nodes, but the file is still stored (and framework resolvers may add per-file
+ * references later, e.g. Drupal routing yml, Spring `@Value` against
+ * application.properties). This is the canonical set behind the no-symbol
+ * branch in `tree-sitter.ts`; `xml` is intentionally excluded because its
+ * MyBatis extractor emits a file node. Callers use this to count such files as
+ * indexed rather than skipped, so it must stay in sync with that branch.
+ */
+export function isFileLevelOnlyLanguage(language: Language): boolean {
+  return language === 'yaml' || language === 'twig' || language === 'properties';
+}
+
 /**
  * Get all supported languages (those with grammar definitions).
  */

+ 6 - 6
src/extraction/index.ts

@@ -17,7 +17,7 @@ import {
 } from '../types';
 import { QueryBuilder } from '../db/queries';
 import { extractFromSource } from './tree-sitter';
-import { detectLanguage, isSourceFile, isLanguageSupported, initGrammars, loadGrammarsForLanguages } from './grammars';
+import { detectLanguage, isSourceFile, isLanguageSupported, isFileLevelOnlyLanguage, initGrammars, loadGrammarsForLanguages } from './grammars';
 import { logDebug, logWarn } from '../errors';
 import { validatePathWithinRoot, normalizePath } from '../utils';
 import ignore, { Ignore } from 'ignore';
@@ -942,11 +942,11 @@ export class ExtractionOrchestrator {
         } else if (result.errors.some((e) => e.severity === 'error')) {
           filesErrored++;
         } else {
-          // Files with no symbols but no errors (e.g. yaml, twig) are tracked
-          // at the file level — count them as indexed so the CLI doesn't
-          // misleadingly report "No files found to index".
+          // Files with no symbols but no errors (yaml, twig, properties) are
+          // tracked at the file level — count them as indexed so the CLI
+          // doesn't misleadingly report "No files found to index".
           const lang = detectLanguage(filePath, content);
-          if (lang === 'yaml' || lang === 'twig') {
+          if (isFileLevelOnlyLanguage(lang)) {
             filesIndexed++;
           } else {
             filesSkipped++;
@@ -1117,7 +1117,7 @@ export class ExtractionOrchestrator {
         filesErrored++;
       } else {
         const tracked = this.queries.getFileByPath(filePath);
-        if (tracked && (tracked.language === 'yaml' || tracked.language === 'twig')) {
+        if (tracked && isFileLevelOnlyLanguage(tracked.language)) {
           filesIndexed++;
         } else {
           filesSkipped++;

+ 2 - 2
src/extraction/tree-sitter.ts

@@ -15,7 +15,7 @@ import {
   ExtractionError,
   UnresolvedReference,
 } from '../types';
-import { getParser, detectLanguage, isLanguageSupported } from './grammars';
+import { getParser, detectLanguage, isLanguageSupported, isFileLevelOnlyLanguage } from './grammars';
 import { generateNodeId, getNodeText, getChildByField, getPrecedingDocstring } from './tree-sitter-helpers';
 import type { LanguageExtractor, ExtractorContext } from './tree-sitter-types';
 import { EXTRACTORS } from './languages';
@@ -3072,7 +3072,7 @@ export function extractFromSource(
     // file node so the watcher tracks it without emitting symbols.
     const extractor = new MyBatisExtractor(filePath, source);
     result = extractor.extract();
-  } else if (detectedLanguage === 'yaml' || detectedLanguage === 'twig' || detectedLanguage === 'properties') {
+  } else if (isFileLevelOnlyLanguage(detectedLanguage)) {
     // No symbol extraction at this stage — files are tracked at the file-record
     // level only. Framework extractors (Drupal routing yml, Spring `@Value`
     // resolution against application.yml/application.properties) run later and