Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions changelog.d/unreleased/3422.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
category: internal
issues:
- 3422
affected:
- src/CodeIndex/Indexer/Extensibility/ExtractorPluginRegistry.cs
- src/CodeIndex/Indexer/Extensibility/ExtractorPluginRegistry.Diagnostics.cs
- src/CodeIndex/Indexer/Extensibility/ExtractorPluginRegistry.Discovery.cs
- src/CodeIndex/Indexer/Extensibility/ExtractorPluginRegistry.PatternConfig.cs
- src/CodeIndex/Indexer/Extensibility/ExtractorPluginRegistry.PluginLoading.cs
---

## English

- **Split extractor plugin registry responsibilities (#3422)** — `ExtractorPluginRegistry` now keeps discovery, pattern config handling, plugin assembly loading, and diagnostic reporting in focused partial files while preserving the existing extension behavior.

## 日本語

- **extractor plugin registry の責務を分割しました (#3422)** — `ExtractorPluginRegistry` は既存の extension 挙動を維持したまま、探索、pattern config 処理、plugin assembly loading、diagnostic reporting を責務別の partial file に分けました。
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using System.Text.Json.Serialization;
using CodeIndex.Diagnostics;

namespace CodeIndex.Indexer.Extensibility;

public static partial class ExtractorPluginRegistry
{
private static void ReportPatternConfigRejected(string path, string reason)
{
Console.Error.WriteLine($"[cdidx] Skipped pattern config '{DiagnosticSanitizer.ForPath(path)}': {DiagnosticSanitizer.ForMessage(reason)}.");
RecordDiagnostic(
"pattern",
path,
typeName: null,
severity: "error",
$"Pattern config skipped: {reason}",
countsAsSkippedFile: true,
category: "invalid_pattern_config");
}

private static void ReportPatternConfigSkipped(string path, string reason)
{
Console.Error.WriteLine($"[cdidx] Skipped pattern config '{DiagnosticSanitizer.ForPath(path)}': {DiagnosticSanitizer.ForMessage(reason)}.");
RecordDiagnostic(
"pattern",
path,
typeName: null,
severity: "skipped",
$"Pattern config skipped: {reason}",
countsAsSkippedFile: true,
category: "pattern_config_incomplete");
}

private static void ReportPatternDirectoryRejected(string path, string reason)
{
Console.Error.WriteLine($"[cdidx] Skipped pattern directory '{DiagnosticSanitizer.ForPath(path)}': {DiagnosticSanitizer.ForMessage(reason)}.");
RecordDiagnostic(
"pattern_directory",
path,
typeName: null,
severity: "error",
$"Pattern directory skipped: {reason}",
countsAsSkippedFile: false,
category: "pattern_directory_rejected");
}

private static void ReportPatternDirectorySkipped(string path, string reason)
{
Console.Error.WriteLine($"[cdidx] Skipped pattern directory '{DiagnosticSanitizer.ForPath(path)}': {DiagnosticSanitizer.ForMessage(reason)}.");
RecordDiagnostic(
"pattern_directory",
path,
typeName: null,
severity: "skipped",
$"Pattern directory skipped: {reason}.",
countsAsSkippedFile: false,
category: "pattern_candidate_limit_exceeded");
}

private static void ReportPluginDirectorySkipped(string path, string reason, string category)
{
RecordDiagnostic(
"plugin_directory",
path,
typeName: null,
severity: "skipped",
$"Plugin directory skipped: {reason}.",
countsAsSkippedFile: false,
category: category);
}

private static void RecordDiagnostic(
string kind,
string path,
string? typeName,
string severity,
string message,
bool countsAsSkippedFile,
string category = "unspecified")
{
lock (Gate)
{
diagnosticTotalCount++;
if (countsAsSkippedFile)
skippedFileCount++;
if (Diagnostics.Count < DiagnosticLimit)
Diagnostics.Add(new ExtractorRegistryDiagnostic(
DiagnosticSanitizer.ForMessage(kind),
DiagnosticSanitizer.ForPath(path),
DiagnosticSanitizer.ForOptionalLabel(typeName),
DiagnosticSanitizer.ForMessage(severity),
DiagnosticSanitizer.ForMessage(category),
DiagnosticSanitizer.ForMessage(message)));
}
}
}

public sealed class ExtractorRegistryStatus
{
[JsonPropertyName("plugin_assembly_count")]
public int PluginAssemblyCount { get; init; }
[JsonPropertyName("pattern_config_count")]
public int PatternConfigCount { get; init; }
[JsonPropertyName("symbol_extractor_count")]
public int SymbolExtractorCount { get; init; }
[JsonPropertyName("reference_extractor_count")]
public int ReferenceExtractorCount { get; init; }
[JsonPropertyName("skipped_file_count")]
public int SkippedFileCount { get; init; }
[JsonPropertyName("diagnostic_count")]
public int DiagnosticCount { get; init; }
[JsonPropertyName("diagnostic_limit")]
public int DiagnosticLimit { get; init; }
[JsonPropertyName("diagnostics_truncated")]
public bool DiagnosticsTruncated { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<ExtractorRegistryDiagnostic>? Diagnostics { get; init; }
}

public sealed record ExtractorRegistryDiagnostic(
[property: JsonPropertyName("kind")] string Kind,
[property: JsonPropertyName("path")] string Path,
[property: JsonPropertyName("type_name")] string? TypeName,
[property: JsonPropertyName("severity")] string Severity,
[property: JsonPropertyName("category")] string Category,
[property: JsonPropertyName("message")] string Message);
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
namespace CodeIndex.Indexer.Extensibility;

public static partial class ExtractorPluginRegistry
{
private static IEnumerable<string> EnumeratePluginAssemblyPaths()
=> EnumeratePluginAssemblyPaths(EnumeratePluginDirectories(projectRoot: null));

private static IEnumerable<string> EnumeratePluginAssemblyPaths(IEnumerable<string> directories)
{
var totalCandidates = 0;
foreach (var directory in directories)
{
if (!Directory.Exists(directory))
continue;

using var enumerator = TryEnumeratePluginFiles(directory);
if (enumerator == null)
continue;

var directoryCandidates = 0;
while (TryMoveNextPluginFile(directory, enumerator, out var pluginPath))
{
if (directoryCandidates >= MaxPluginAssemblyCandidatesPerDirectory)
{
ReportPluginDirectorySkipped(
directory,
$"too many plugin assembly candidates (maximum {MaxPluginAssemblyCandidatesPerDirectory} per directory)",
"plugin_candidate_limit_exceeded");
break;
}

if (totalCandidates >= MaxPluginAssemblyCandidatesTotal)
{
ReportPluginDirectorySkipped(
directory,
$"too many plugin assembly candidates (maximum {MaxPluginAssemblyCandidatesTotal} total)",
"plugin_candidate_limit_exceeded");
yield break;
}

directoryCandidates++;
totalCandidates++;
yield return pluginPath;
}
}
}

private static IEnumerator<string>? TryEnumeratePluginFiles(string directory)
{
try
{
return Directory.EnumerateFiles(directory, "*.dll", SearchOption.TopDirectoryOnly).GetEnumerator();
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
ReportPluginDirectorySkipped(directory, "could not enumerate plugin directory", "plugin_directory_enumeration_failed");
return null;
}
}

private static bool TryMoveNextPluginFile(string directory, IEnumerator<string> enumerator, out string pluginPath)
{
pluginPath = string.Empty;
try
{
if (!enumerator.MoveNext())
return false;

pluginPath = enumerator.Current;
return true;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
ReportPluginDirectorySkipped(directory, "could not enumerate plugin directory", "plugin_directory_enumeration_failed");
return false;
}
}

private static IEnumerable<string> EnumeratePluginDirectories(string? projectRoot)
{
if (WorkspacePluginsTrusted() && !string.IsNullOrWhiteSpace(projectRoot))
{
foreach (var directory in EnumerateWorkspacePluginDirectories(Path.GetFullPath(projectRoot)))
yield return directory;
}

var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (!string.IsNullOrWhiteSpace(home))
yield return Path.Combine(home, ".cdidx", "plugins");
}

private static IEnumerable<string> EnumerateWorkspacePluginDirectories(string projectRoot)
{
yield return Path.Combine(projectRoot, ".cdidx", "plugins");
}

private static IEnumerable<string> EnumeratePatternConfigPaths(string workspaceRoot, bool includeUserDirectory = true)
{
foreach (var path in EnumeratePatternConfigPathsFromDirectory(
Path.Combine(workspaceRoot, ".cdidx", "patterns"),
workspaceRoot))
{
yield return path;
}

if (!includeUserDirectory)
yield break;

foreach (var path in EnumerateUserPatternConfigPaths())
yield return path;
}

private static IEnumerable<string> EnumerateUserPatternConfigPaths()
{
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (!string.IsNullOrWhiteSpace(home))
{
foreach (var path in EnumeratePatternConfigPathsFromDirectory(
Path.Combine(home, ".config", "cdidx", "patterns"),
workspaceRoot: null))
{
yield return path;
}
}
}

private static IEnumerable<string> EnumeratePatternConfigPathsFromDirectory(string directory, string? workspaceRoot)
{
if (!Directory.Exists(directory) || !PatternDirectoryIsSafe(directory, workspaceRoot))
yield break;

var directoryCandidates = 0;
foreach (var searchPattern in PatternConfigSearchPatterns)
{
using var enumerator = TryEnumeratePatternFiles(directory, searchPattern);
if (enumerator == null)
continue;

while (TryMoveNextPatternFile(directory, enumerator, out var path))
{
if (directoryCandidates >= MaxPatternConfigCandidatesPerDirectory)
{
ReportPatternDirectorySkipped(
directory,
$"too many pattern config candidates (maximum {MaxPatternConfigCandidatesPerDirectory} per directory)");
yield break;
}

directoryCandidates++;
yield return path;
}
}
}

private static IEnumerator<string>? TryEnumeratePatternFiles(string directory, string searchPattern)
{
try
{
return Directory.EnumerateFiles(directory, searchPattern, SearchOption.TopDirectoryOnly).GetEnumerator();
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
ReportPatternDirectoryRejected(directory, "could not enumerate pattern directory");
return null;
}
}

private static bool TryMoveNextPatternFile(string directory, IEnumerator<string> enumerator, out string patternPath)
{
patternPath = string.Empty;
try
{
if (!enumerator.MoveNext())
return false;

patternPath = enumerator.Current;
return true;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
ReportPatternDirectoryRejected(directory, "could not enumerate pattern directory");
return false;
}
}

private static bool PatternDirectoryIsSafe(string directory, string? workspaceRoot)
{
if (workspaceRoot != null)
{
var workspaceCdidxDirectory = Path.Combine(workspaceRoot, ".cdidx");
if (DirectoryIsSymlinkOrReparsePoint(workspaceCdidxDirectory))
{
ReportPatternDirectoryRejected(workspaceCdidxDirectory, "symbolic links and reparse points are not supported");
return false;
}
}

if (DirectoryIsSymlinkOrReparsePoint(directory))
{
ReportPatternDirectoryRejected(directory, "symbolic links and reparse points are not supported");
return false;
}

return true;
}

private static bool DirectoryIsSymlinkOrReparsePoint(string directory)
{
try
{
var info = new DirectoryInfo(directory);
return (info.Attributes & FileAttributes.ReparsePoint) != 0
|| !string.IsNullOrEmpty(info.LinkTarget);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
ReportPatternDirectoryRejected(directory, "could not inspect pattern directory");
return true;
}
}

private static bool WorkspacePluginsTrusted()
{
var value = Environment.GetEnvironmentVariable(TrustWorkspacePluginsEnvironmentVariable);
return value != null
&& (value.Equals("1", StringComparison.OrdinalIgnoreCase)
|| value.Equals("true", StringComparison.OrdinalIgnoreCase)
|| value.Equals("yes", StringComparison.OrdinalIgnoreCase)
|| value.Equals("on", StringComparison.OrdinalIgnoreCase));
}
}
Loading
Loading