diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 20b6f6fdb4..0c8ed561ae 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1786,7 +1786,7 @@ All indexed languages are searchable through FTS5. Rows with **Symbols = yes** a | Raku | `.raku`, `.rakumod`, `.rakutest` | -- | | Perl test | `.t` | -- | | Zig | `.zig` | yes | -| XAML | `.xaml`, `.axaml` | -- | +| XAML | `.xaml`, `.axaml` | yes | | MSBuild | `.csproj`, `.fsproj`, `.vbproj`, `.props`, `.targets` | yes | | Shell | `.sh`, `.bash`, `.zsh`, `.fish` | partial | | PowerShell | `.ps1`, `.psm1`, `.psd1` | yes | @@ -1800,8 +1800,8 @@ All indexed languages are searchable through FTS5. Rows with **Symbols = yes** a | TOML | `.toml` | -- | | HTML | `.html`, `.htm`, `.xhtml`, `.shtml` | yes | | CSS | `.css`, `.scss`, `.less`, `.pcss` | yes | -| Sass (indented) | `.sass` | -- | -| Stylus | `.styl` | -- | +| Sass (indented) | `.sass` | yes | +| Stylus | `.styl` | yes | | Vue | `.vue` | -- | | Svelte | `.svelte` | -- | | Terraform | `.tf` | -- | @@ -1842,7 +1842,7 @@ commands and when to fall back to `search`. | C / C++ / Objective-C / Swift / Rust / Go / Zig | functions, types, methods, imports/modules | calls, constructors, macro invocations where supported, type references | C++ templates/macros and Rust macro expansion are not evaluated; Rust macro invocations are still reference edges. | | Shell / PowerShell / Batch / Makefile / CMake / Justfile / MSBuild / Gradle | functions, labels, targets, recipes, tasks, imports where applicable | command-style calls, target dependencies, and control-flow targets | Runtime command construction is not resolved. | | SQL / Terraform / Dockerfile | statements/resources/stages/labels | table/resource/stage references, Dockerfile stage dependencies, Terraform dotted refs | SQL hotspot grouping defaults to statements; Dockerfile `COPY --from=` follows named stages. | -| Markdown / HTML / CSS / GraphQL / Protobuf | headings, anchors, selectors, schema types/messages where supported | links/assets/components, local anchors, CSS extends/variables, schema references where supported | Use `search` for prose and generated markup. | +| Markdown / HTML / CSS / Sass / Stylus / XAML / GraphQL / Protobuf | headings, anchors, selectors, UI elements, schema types/messages where supported | links/assets/components, local anchors, CSS/Sass/Stylus imports, variables, mixins/functions, XAML resources/bindings/handlers, schema references where supported | Use `search` for prose and generated markup. | | Dependency manifests / lockfiles | none | none | Use `--lang dependency_manifest` or `--lang dependency_lock` for dependency/security audits. | | Other indexed text formats | file/chunk search only unless `languages` reports symbols | no graph unless `languages` reports support | `cdidx search "literal" --lang yaml` is the reliable fallback. | @@ -4243,7 +4243,7 @@ indexing はファイル単位の SQLite transaction を commit します。長 | Raku | `.raku`, `.rakumod`, `.rakutest` | -- | | Perl test | `.t` | -- | | Zig | `.zig` | yes | -| XAML | `.xaml`, `.axaml` | -- | +| XAML | `.xaml`, `.axaml` | yes | | MSBuild | `.csproj`, `.fsproj`, `.vbproj`, `.props`, `.targets` | yes | | Shell | `.sh`, `.bash`, `.zsh`, `.fish` | partial | | PowerShell | `.ps1`, `.psm1`, `.psd1` | yes | @@ -4257,8 +4257,8 @@ indexing はファイル単位の SQLite transaction を commit します。長 | TOML | `.toml` | -- | | HTML | `.html`, `.htm`, `.xhtml`, `.shtml` | yes | | CSS | `.css`, `.scss`, `.less`, `.pcss` | yes | -| Sass(インデント構文) | `.sass` | -- | -| Stylus | `.styl` | -- | +| Sass(インデント構文) | `.sass` | yes | +| Stylus | `.styl` | yes | | Vue | `.vue` | -- | | Svelte | `.svelte` | -- | | Terraform | `.tf` | -- | @@ -4296,7 +4296,7 @@ indexing はファイル単位の SQLite transaction を commit します。長 | C / C++ / Objective-C / Swift / Rust / Go / Zig | function、type、method、import/module | call、constructor、対応言語の macro invocation、type reference | C++ template/macro と Rust macro expansion は評価しません。Rust macro invocation 自体は reference edge です。 | | Shell / PowerShell / Batch / Makefile / CMake / Justfile / MSBuild / Gradle | function、label、target、recipe、task、対応言語の import | command-style call、target dependency、control-flow target | runtime で組み立てられる command は解決しません。 | | SQL / Terraform / Dockerfile | statement/resource/stage/label | table/resource/stage reference、Dockerfile stage dependency、Terraform dotted refs | SQL hotspot grouping は既定で statement、Dockerfile `COPY --from=` は named stage を追跡します。 | -| Markdown / HTML / CSS / GraphQL / Protobuf | heading、anchor、selector、対応 schema type/message | link/asset/component、local anchor、CSS extend/variable、対応 schema reference | prose や generated markup には `search` を使ってください。 | +| Markdown / HTML / CSS / Sass / Stylus / XAML / GraphQL / Protobuf | heading、anchor、selector、UI element、対応 schema type/message | link/asset/component、local anchor、CSS/Sass/Stylus の import・variable・mixin/function、XAML resource / binding / handler、対応 schema reference | prose や generated markup には `search` を使ってください。 | | Dependency manifest / lockfile | なし | なし | dependency / security audit には `--lang dependency_manifest` または `--lang dependency_lock` を使います。 | | その他の indexed text format | `languages` が symbol 対応を示す場合を除き file/chunk search のみ | `languages` が graph 対応を示す場合を除きなし | `cdidx search "literal" --lang yaml` が信頼できる fallback です。 | diff --git a/changelog.d/unreleased/3534.added.md b/changelog.d/unreleased/3534.added.md new file mode 100644 index 0000000000..cac103a07a --- /dev/null +++ b/changelog.d/unreleased/3534.added.md @@ -0,0 +1,19 @@ +--- +category: added +issues: + - 3534 +affected: + - src/CodeIndex/Indexer/Scanning/FileIndexer.cs + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs + - src/CodeIndex/Indexer/References/Languages/CssReferenceExtractor.cs +--- + +## English + +- **Sass and Stylus now expose preprocessor navigation anchors (#3534)** — `.sass` and `.styl` files now surface conservative symbol and reference records for imports, variables, mixins, functions, selectors, and related call sites. + +## 日本語 + +- **Sass と Stylus がプリプロセッサのナビゲーションアンカーを公開するようになりました (#3534)** — `.sass` と `.styl` ファイルで、import、変数、mixin、関数、セレクタ、および関連する呼び出し箇所の保守的な symbol / reference record を出力するようになりました。 diff --git a/changelog.d/unreleased/3535.added.md b/changelog.d/unreleased/3535.added.md new file mode 100644 index 0000000000..63f09ea19e --- /dev/null +++ b/changelog.d/unreleased/3535.added.md @@ -0,0 +1,18 @@ +--- +category: added +issues: + - 3535 +affected: + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs + - src/CodeIndex/Indexer/References/Languages/XamlReferenceExtractor.cs +--- + +## English + +- **XAML and AXAML now expose UI navigation references (#3535)** — XML-backed XAML files now surface conservative type, resource, binding, element, and event-handler references for WPF and Avalonia navigation. + +## 日本語 + +- **XAML と AXAML が UI ナビゲーション参照を公開するようになりました (#3535)** — XML ベースの XAML ファイルで、WPF と Avalonia の型、リソース、バインディング、要素、イベントハンドラへの保守的な reference record を出力するようになりました。 diff --git a/src/CodeIndex/Indexer/References/Languages/CssReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/CssReferenceExtractor.cs index d41c87ef73..dc2e98009b 100644 --- a/src/CodeIndex/Indexer/References/Languages/CssReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/CssReferenceExtractor.cs @@ -20,6 +20,38 @@ internal static class CssReferenceExtractor @"@include\s+(?[A-Za-z_][\w-]*)", RegexOptions.Compiled); + private static readonly Regex SassIndentedMixinReferenceRegex = new( + @"(?[A-Za-z_][\w-]*)", + RegexOptions.Compiled); + + private static readonly Regex SassBareFunctionReferenceRegex = new( + @"(?[A-Za-z_][\w-]*)\s*\(", + RegexOptions.Compiled); + + private static readonly Regex StylusVariableReferenceRegex = new( + @"(?[A-Za-z_][\w-]*)", + RegexOptions.Compiled); + + private static readonly Regex StylusBareVariableReferenceRegex = new( + @"(?[A-Za-z_][\w-]*)(?![\w-])", + RegexOptions.Compiled); + + private static readonly Regex StylusBareFunctionReferenceRegex = new( + @"(?[A-Za-z_][\w-]*)\s*\(", + RegexOptions.Compiled); + + private static readonly Regex StylusVariableDefinitionRegex = new( + @"^\s*\$?(?[A-Za-z_][\w-]*)\s*(?:=|:=)\s*", + RegexOptions.Compiled); + + private static readonly Regex SassImportReferenceRegex = new( + @"^\s*@(?>import|use|forward)\s+(?:url\(\s*)?(?:""(?[^""]+)""|'(?[^']+)'|(?[^\s)""';]+))", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + private static readonly Regex StylusImportReferenceRegex = new( + @"^\s*@(?>import|require|use)\s+(?:url\(\s*)?(?:""(?[^""]+)""|'(?[^']+)'|(?[^\s)""';]+))", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + private static readonly Regex CssCustomPropertyReferenceRegex = new(@"\bvar\(\s*--(?[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private static readonly Regex CssAnimationNameValueRegex = new(@"\banimation-name\s*:\s*(?[^;{}]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private static readonly Regex CssAnimationShorthandValueRegex = new(@"\banimation\s*:\s*(?[^;{}]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); @@ -57,6 +89,11 @@ internal static class CssReferenceExtractor "initial", "inherit", "unset", "revert", "revert-layer", }; + private static readonly HashSet CssBuiltInFunctionNames = new(StringComparer.OrdinalIgnoreCase) + { + "url", "var", "calc", "rgb", "rgba", "hsl", "hsla", + }; + public static void EmitCss( string preparedLine, string originalLine, @@ -148,6 +185,369 @@ public static void EmitScss( EmitMatches(pattern, preparedLine, context, lineNumber, references, seen, fileId, definitionNames: null, container); } + public static void EmitSass( + string preparedLine, + string originalLine, + List references, + HashSet seen, + long fileId, + string context, + int lineNumber, + SymbolRecord? container) + { + var sassReferenceLine = PrepareSassStylusReferenceLine(originalLine); + if (ShouldSkipSassIndentedDeclarationReferences(sassReferenceLine)) + sassReferenceLine = ""; + + EmitScss(sassReferenceLine, references, seen, fileId, context, lineNumber, container); + EmitPreprocessorImportReferences(SassImportReferenceRegex, originalLine, references, seen, fileId, context, lineNumber, container); + + var sassMixinReferenceLine = ShouldSkipSassIndentedDeclarationReferences(sassReferenceLine) + ? "" + : sassReferenceLine; + + foreach (Match match in BoundedRegex.EnumerateMatches(SassIndentedMixinReferenceRegex, sassMixinReferenceLine)) + { + ReferenceExtractor.AddReference( + references, + seen, + fileId, + match, + "call", + context, + lineNumber, + container); + } + + foreach (Match match in BoundedRegex.EnumerateMatches(SassBareFunctionReferenceRegex, sassReferenceLine)) + { + var name = match.Groups["name"].Value; + if (CssBuiltInFunctionNames.Contains(name)) + continue; + if (ShouldSkipSassBareFunctionReference(sassReferenceLine, match.Groups["name"].Index)) + continue; + + ReferenceExtractor.AddReference( + references, + seen, + fileId, + match, + "call", + context, + lineNumber, + container); + } + } + + public static void EmitStylus( + string preparedLine, + string originalLine, + List references, + HashSet seen, + long fileId, + string context, + int lineNumber, + HashSet? definitionNames, + HashSet? variableDefinitionNames, + SymbolRecord? container) + { + EmitPreprocessorImportReferences(StylusImportReferenceRegex, originalLine, references, seen, fileId, context, lineNumber, container); + + var stylusReferenceLine = PrepareSassStylusReferenceLine(originalLine); + foreach (Match match in BoundedRegex.EnumerateMatches(StylusVariableReferenceRegex, stylusReferenceLine)) + { + if (ShouldSkipStylusVariableReference(stylusReferenceLine, match.Groups["name"].Index)) + continue; + + ReferenceExtractor.AddReference( + references, + seen, + fileId, + match, + "call", + context, + lineNumber, + container); + } + + if (variableDefinitionNames != null) + { + foreach (Match match in BoundedRegex.EnumerateMatches(StylusBareVariableReferenceRegex, stylusReferenceLine)) + { + var nameGroup = match.Groups["name"]; + if (!variableDefinitionNames.Contains(nameGroup.Value)) + continue; + if (ShouldSkipStylusBareVariableReference(stylusReferenceLine, nameGroup.Index)) + continue; + + ReferenceExtractor.AddReference( + references, + seen, + fileId, + nameGroup.Value, + nameGroup.Index, + "call", + context, + lineNumber, + container); + } + } + + foreach (Match match in BoundedRegex.EnumerateMatches(StylusBareFunctionReferenceRegex, stylusReferenceLine)) + { + var name = match.Groups["name"].Value; + if (CssBuiltInFunctionNames.Contains(name)) + continue; + if (definitionNames != null && definitionNames.Contains(name) && match.Groups["name"].Index == 0) + continue; + + ReferenceExtractor.AddReference( + references, + seen, + fileId, + match, + "call", + context, + lineNumber, + container); + } + } + + internal static HashSet BuildStylusVariableDefinitionNames(string[] lines) + { + var names = new HashSet(StringComparer.Ordinal); + var inBlockComment = false; + + foreach (var line in lines) + { + var blockMaskedLine = MaskSassStylusBlockCommentLine(line, ref inBlockComment); + var referenceLine = PrepareSassStylusReferenceLine(blockMaskedLine); + var match = StylusVariableDefinitionRegex.Match(referenceLine); + if (match.Success) + names.Add(match.Groups["name"].Value); + } + + return names; + } + + internal sealed class SassLoudCommentState + { + public bool Active { get; set; } + public int Indent { get; set; } + public bool SilentActive { get; set; } + public int SilentIndent { get; set; } + } + + internal static string MaskSassBlockCommentLine(string line, SassLoudCommentState state) + { + var chars = line.ToCharArray(); + var lineIndent = CountLeadingWhitespace(line); + var isBlank = lineIndent == line.Length; + var cursor = 0; + + if (state.SilentActive) + { + if (isBlank || lineIndent > state.SilentIndent) + { + for (var j = 0; j < chars.Length; j++) + chars[j] = ' '; + return new string(chars); + } + + state.SilentActive = false; + } + + if (state.Active) + { + var trimmed = isBlank ? "" : line[lineIndent..]; + if (!isBlank + && lineIndent <= state.Indent + && !trimmed.StartsWith("*/", StringComparison.Ordinal)) + { + state.Active = false; + } + else + { + var commentEnd = line.IndexOf("*/", StringComparison.Ordinal); + if (commentEnd < 0) + { + for (var j = 0; j < chars.Length; j++) + chars[j] = ' '; + return new string(chars); + } + + var stop = commentEnd + 2; + for (var j = 0; j < stop; j++) + chars[j] = ' '; + cursor = stop; + state.Active = false; + } + } + + var quote = '\0'; + var parenDepth = 0; + for (var i = cursor; i < line.Length; i++) + { + var ch = line[i]; + if (quote != '\0') + { + if (ch == '\\' && i + 1 < line.Length) + { + i++; + continue; + } + + if (ch == quote) + quote = '\0'; + continue; + } + + if (ch is '\'' or '"') + { + quote = ch; + continue; + } + + if (ch == '(') + { + parenDepth++; + continue; + } + + if (ch == ')' && parenDepth > 0) + { + parenDepth--; + continue; + } + + if (ch == '/' && i + 1 < line.Length && line[i + 1] == '*') + { + var commentEnd = line.IndexOf("*/", i + 2, StringComparison.Ordinal); + var stop = commentEnd >= 0 ? commentEnd + 2 : line.Length; + for (var j = i; j < stop; j++) + chars[j] = ' '; + + if (commentEnd < 0) + { + state.Active = true; + state.Indent = lineIndent; + break; + } + + i = stop - 1; + continue; + } + + if (parenDepth == 0 && ch == '/' && i + 1 < line.Length && line[i + 1] == '/') + { + for (var j = i; j < chars.Length; j++) + chars[j] = ' '; + if (i == lineIndent) + { + state.SilentActive = true; + state.SilentIndent = lineIndent; + } + break; + } + } + + return new string(chars); + } + + internal static string MaskSassStylusBlockCommentLine(string line, ref bool inBlockComment) + { + var chars = line.ToCharArray(); + var quote = '\0'; + + for (var i = 0; i < line.Length; i++) + { + var ch = line[i]; + + if (inBlockComment) + { + chars[i] = ' '; + if (ch == '*' && i + 1 < line.Length && line[i + 1] == '/') + { + chars[i + 1] = ' '; + inBlockComment = false; + i++; + } + + continue; + } + + if (quote != '\0') + { + if (ch == '\\' && i + 1 < line.Length) + { + i++; + continue; + } + + if (ch == quote) + quote = '\0'; + continue; + } + + if (ch is '\'' or '"') + { + quote = ch; + continue; + } + + if (ch == '/' && i + 1 < line.Length && line[i + 1] == '*') + { + chars[i] = ' '; + chars[i + 1] = ' '; + inBlockComment = true; + i++; + } + } + + return new string(chars); + } + + private static int CountLeadingWhitespace(string line) + { + var index = 0; + while (index < line.Length && char.IsWhiteSpace(line[index])) + index++; + return index; + } + + private static void EmitPreprocessorImportReferences( + Regex importRegex, + string originalLine, + List references, + HashSet seen, + long fileId, + string context, + int lineNumber, + SymbolRecord? container) + { + if (originalLine.TrimStart().StartsWith("//", StringComparison.Ordinal)) + return; + + var importScanLine = CssInlineBlockCommentRegex.Replace(originalLine, " "); + foreach (Match match in importRegex.Matches(importScanLine)) + { + var nameGroup = match.Groups["name"]; + if (!nameGroup.Success || nameGroup.Value.Length == 0) + continue; + + ReferenceExtractor.AddReference( + references, + seen, + fileId, + nameGroup.Value, + nameGroup.Index, + "import", + context, + lineNumber, + container); + } + } + private static void EmitMatches( ReferencePattern pattern, string preparedLine, @@ -707,4 +1107,121 @@ private static bool ShouldSkipScssVariableReference(string preparedLine, int var return false; } + + private static bool ShouldSkipSassIndentedDeclarationReferences(string preparedLine) + { + var trimmed = preparedLine.TrimStart(); + return trimmed.StartsWith("=", StringComparison.Ordinal); + } + + private static bool ShouldSkipSassBareFunctionReference(string preparedLine, int functionIndex) + { + var trimmed = preparedLine.TrimStart(); + if (!trimmed.StartsWith("@function", StringComparison.Ordinal) + && !trimmed.StartsWith("@mixin", StringComparison.Ordinal)) + { + return false; + } + + var firstNonWhitespace = preparedLine.Length - trimmed.Length; + return functionIndex >= firstNonWhitespace; + } + + private static string PrepareSassStylusReferenceLine(string originalLine) + { + var chars = originalLine.ToCharArray(); + char quote = '\0'; + var parenDepth = 0; + for (var i = 0; i < originalLine.Length; i++) + { + var ch = originalLine[i]; + if (quote != '\0') + { + chars[i] = ' '; + if (ch == quote && (i == 0 || originalLine[i - 1] != '\\')) + quote = '\0'; + continue; + } + + if (ch is '\'' or '"') + { + chars[i] = ' '; + quote = ch; + continue; + } + + if (ch == '/' && i + 1 < originalLine.Length && originalLine[i + 1] == '*') + { + var commentEnd = originalLine.IndexOf("*/", i + 2, StringComparison.Ordinal); + var stop = commentEnd >= 0 ? commentEnd + 2 : originalLine.Length; + for (var j = i; j < stop; j++) + chars[j] = ' '; + i = stop - 1; + continue; + } + + if (ch == '(') + { + parenDepth++; + continue; + } + + if (ch == ')' && parenDepth > 0) + { + parenDepth--; + continue; + } + + if (parenDepth == 0 && ch == '/' && i + 1 < originalLine.Length && originalLine[i + 1] == '/') + { + for (var j = i; j < originalLine.Length; j++) + chars[j] = ' '; + break; + } + } + + return new string(chars); + } + + private static bool ShouldSkipStylusVariableReference(string preparedLine, int variableIndex) + { + var firstNonWhitespace = 0; + while (firstNonWhitespace < preparedLine.Length && char.IsWhiteSpace(preparedLine[firstNonWhitespace])) + firstNonWhitespace++; + + var dollarIndex = variableIndex - 1; + if (dollarIndex != firstNonWhitespace || dollarIndex < 0 || preparedLine[dollarIndex] != '$') + return false; + + var cursor = variableIndex; + while (cursor < preparedLine.Length && (char.IsLetterOrDigit(preparedLine[cursor]) || preparedLine[cursor] is '_' or '-')) + cursor++; + while (cursor < preparedLine.Length && char.IsWhiteSpace(preparedLine[cursor])) + cursor++; + + return cursor < preparedLine.Length + && (preparedLine[cursor] == '=' + || (preparedLine[cursor] == ':' && cursor + 1 < preparedLine.Length && preparedLine[cursor + 1] == '=')); + } + + private static bool ShouldSkipStylusBareVariableReference(string preparedLine, int variableIndex) + { + var firstNonWhitespace = 0; + while (firstNonWhitespace < preparedLine.Length && char.IsWhiteSpace(preparedLine[firstNonWhitespace])) + firstNonWhitespace++; + if (variableIndex == firstNonWhitespace) + return true; + + var cursor = variableIndex; + while (cursor < preparedLine.Length && (char.IsLetterOrDigit(preparedLine[cursor]) || preparedLine[cursor] is '_' or '-')) + cursor++; + while (cursor < preparedLine.Length && char.IsWhiteSpace(preparedLine[cursor])) + cursor++; + + if (cursor < preparedLine.Length && preparedLine[cursor] == '(') + return true; + return cursor < preparedLine.Length + && (preparedLine[cursor] == '=' + || (preparedLine[cursor] == ':' && cursor + 1 < preparedLine.Length && preparedLine[cursor + 1] == '=')); + } } diff --git a/src/CodeIndex/Indexer/References/Languages/XamlReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/XamlReferenceExtractor.cs new file mode 100644 index 0000000000..23f88eee09 --- /dev/null +++ b/src/CodeIndex/Indexer/References/Languages/XamlReferenceExtractor.cs @@ -0,0 +1,899 @@ +using System.Text; +using System.Text.RegularExpressions; +using CodeIndex.Models; +using Regex = CodeIndex.Indexer.BoundedRegex; + +namespace CodeIndex.Indexer; + +internal static class XamlReferenceExtractor +{ + private static readonly Regex XamlTypeAttributeRegex = new( + @"\b(?x:Class|x:DataType|TargetType|DataType)\s*=\s*[""'](?[^""']+)[""']", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly Regex XamlTypeArgumentsRegex = new( + @"\bx:TypeArguments\s*=\s*[""'](?[^""']+)[""']", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly Regex XamlResourceReferenceRegex = new( + @"\{(?:StaticResource|StaticResourceExtension|DynamicResource|DynamicResourceExtension)\b(?(?:[^{}]|{[^{}]*})*)\}", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly Regex XamlReferenceRegex = new( + @"\{x:Reference(?:Extension)?\b(?(?:[^{}]|{[^{}]*})*)\}", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly Regex XamlBindingElementRegex = new( + @"<\s*(?:[A-Za-z_][\w.-]*:)?Binding\b(?[^<>]*)", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly Regex XamlBindingPropertyElementRegex = new( + @"<\s*(?:[A-Za-z_][\w.-]*:)?Binding\.(?Path|ElementName)\s*>\s*(?[^<]+?)\s*\s*>", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + private static readonly Regex XamlBindingPropertyElementStartRegex = new( + @"<\s*(?:[A-Za-z_][\w.-]*:)?Binding\.(?Path|ElementName)\s*>\s*(?.*)$", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + private static readonly Regex XamlBindingPropertyElementEndRegex = new( + @"Path|ElementName)\s*>", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + private static readonly Regex XamlAttributeRegex = new( + @"\b(?[A-Za-z_][\w:.-]*)\s*=\s*[""'](?[^""']+)[""']", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly Regex XamlEventHandlerRegex = new( + @"\b(?:Click|Clicked|Tapped|Loaded|Unloaded|SelectionChanged|TextChanged|CheckedChanged|Unchecked|SelectedIndexChanged|PointerPressed|PointerReleased|PointerEntered|PointerExited|Drop|DragOver|Completed|Appearing|Disappearing|NavigatedTo|NavigatedFrom|SizeChanged)\s*=\s*[""'](?[^""']+)[""']", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + public static bool IsXaml(string[] lines) + { + foreach (var line in lines) + { + if (line.IndexOf("schemas.microsoft.com/winfx/2006/xaml", StringComparison.OrdinalIgnoreCase) >= 0 + || line.IndexOf("schemas.microsoft.com/winfx/2009/xaml", StringComparison.OrdinalIgnoreCase) >= 0 + || line.IndexOf("schemas.microsoft.com/dotnet/2021/maui", StringComparison.OrdinalIgnoreCase) >= 0 + || line.IndexOf("xamarin.com/schemas/2014/forms", StringComparison.OrdinalIgnoreCase) >= 0 + || line.IndexOf("github.com/avaloniaui", StringComparison.OrdinalIgnoreCase) >= 0) + return true; + } + + return false; + } + + public static string StripXmlComments(string line, ref bool inComment) + { + var builder = new StringBuilder(line.Length); + var index = 0; + + while (index < line.Length) + { + if (inComment) + { + var commentEnd = line.IndexOf("-->", index, StringComparison.Ordinal); + if (commentEnd < 0) + { + builder.Append(' ', line.Length - index); + return builder.ToString(); + } + + builder.Append(' ', commentEnd + 3 - index); + index = commentEnd + 3; + inComment = false; + continue; + } + + var commentStart = line.IndexOf("", commentStart + 4, StringComparison.Ordinal); + if (sameLineCommentEnd < 0) + { + builder.Append(' ', line.Length - commentStart); + inComment = true; + return builder.ToString(); + } + + builder.Append(' ', sameLineCommentEnd + 3 - commentStart); + index = sameLineCommentEnd + 3; + } + + return builder.ToString(); + } + + public static void Emit( + string originalLine, + string context, + int lineNumber, + List references, + HashSet seen, + long fileId, + SymbolRecord? container, + BindingPropertyElementState bindingPropertyElementState, + BindingMarkupExtensionState bindingMarkupExtensionState) + { + if (bindingPropertyElementState.Active) + { + if (bindingPropertyElementState.Advance(originalLine, context, lineNumber, container, out var completed)) + { + var completedValue = NormalizeBindingPath(completed.Value); + if (completedValue.Length > 0) + AddReference(references, seen, fileId, completedValue, completed.ValueIndex, "reference", completed.Context, completed.LineNumber, completed.Container); + } + + return; + } + + if (bindingMarkupExtensionState.Active) + { + if (bindingMarkupExtensionState.Advance(originalLine, context, lineNumber, container, out var completed)) + { + EmitBindingMarkupExtensionReferences(completed.Kind, completed.Content, completed.ContentIndex, references, seen, fileId, completed.Context, completed.LineNumber, completed.Container); + if (!string.IsNullOrWhiteSpace(completed.RemainingLine)) + Emit(completed.RemainingLine, context, lineNumber, references, seen, fileId, container, bindingPropertyElementState, bindingMarkupExtensionState); + } + + return; + } + + if (originalLine.Length == 0) + return; + + foreach (Match match in XamlTypeAttributeRegex.Matches(originalLine)) + { + foreach (var name in NormalizeXamlTypeValues(match.Groups["value"].Value)) + { + AddReference(references, seen, fileId, name, match.Groups["value"].Index, "type_reference", context, lineNumber, container); + } + } + + foreach (Match match in XamlTypeArgumentsRegex.Matches(originalLine)) + { + foreach (var name in NormalizeXamlTypeArguments(match.Groups["value"].Value)) + { + AddReference(references, seen, fileId, name, match.Groups["value"].Index, "type_reference", context, lineNumber, container); + } + } + + foreach (var resource in EnumerateResourceMarkupExtensions(originalLine)) + { + var value = NormalizeNamedMarkupReference(resource.Content); + if (value.Length > 0) + AddReference(references, seen, fileId, value, resource.ContentIndex, "reference", context, lineNumber, container); + } + + foreach (Match match in XamlReferenceRegex.Matches(originalLine)) + { + var value = NormalizeNamedMarkupReference(match.Groups["content"].Value); + if (value.Length > 0) + AddReference(references, seen, fileId, value, match.Groups["content"].Index, "reference", context, lineNumber, container); + } + + foreach (var binding in EnumerateBindingMarkupExtensions(originalLine)) + { + EmitBindingMarkupExtensionReferences(binding.Kind, binding.Content, binding.ContentIndex, references, seen, fileId, context, lineNumber, container); + } + + TryStartBindingMarkupExtensionState(originalLine, context, lineNumber, container, bindingMarkupExtensionState); + + foreach (Match match in XamlBindingElementRegex.Matches(originalLine)) + { + EmitBindingElementAttributeReferences( + match.Groups["attributes"].Value, + match.Groups["attributes"].Index, + references, + seen, + fileId, + context, + lineNumber, + container); + } + + foreach (Match match in XamlBindingPropertyElementRegex.Matches(originalLine)) + { + var value = NormalizeBindingPath(match.Groups["value"].Value); + if (value.Length > 0) + AddReference(references, seen, fileId, value, match.Groups["value"].Index, "reference", context, lineNumber, container); + } + + TryStartBindingPropertyElementState(originalLine, context, lineNumber, container, bindingPropertyElementState); + + foreach (Match match in XamlEventHandlerRegex.Matches(originalLine)) + { + var value = match.Groups["value"].Value.Trim(); + if (value.Length > 0) + AddReference(references, seen, fileId, value, match.Groups["value"].Index, "call", context, lineNumber, container); + } + } + + private static void EmitBindingMarkupExtensionReferences( + string kind, + string content, + int contentIndex, + List references, + HashSet seen, + long fileId, + string context, + int lineNumber, + SymbolRecord? container) + { + foreach (var name in NormalizeBindingReferences(kind, content)) + AddReference(references, seen, fileId, name, contentIndex, "reference", context, lineNumber, container); + } + + private static void TryStartBindingMarkupExtensionState( + string line, + string context, + int lineNumber, + SymbolRecord? container, + BindingMarkupExtensionState state) + { + if (!TryFindUnclosedBindingMarkupExtension(line, out var start)) + return; + + state.Start( + start.Kind, + start.InitialContent, + start.InitialContentIndex, + start.InitialBraceDepth, + context, + lineNumber, + container); + } + + private static void TryStartBindingPropertyElementState( + string line, + string context, + int lineNumber, + SymbolRecord? container, + BindingPropertyElementState state) + { + var match = XamlBindingPropertyElementStartRegex.Match(line); + if (!match.Success) + return; + + var property = match.Groups["property"].Value; + var tailGroup = match.Groups["tail"]; + if (TryFindBindingPropertyElementEnd(tailGroup.Value, property, out _)) + return; + + state.Start(property, tailGroup.Value, tailGroup.Index, context, lineNumber, container); + } + + private static bool TryFindBindingPropertyElementEnd(string line, string property, out Match closingTag) + { + foreach (Match match in XamlBindingPropertyElementEndRegex.Matches(line)) + { + if (match.Groups["property"].Value.Equals(property, StringComparison.OrdinalIgnoreCase)) + { + closingTag = match; + return true; + } + } + + closingTag = Match.Empty; + return false; + } + + internal sealed class BindingPropertyElementState + { + private readonly StringBuilder value = new(); + private string property = ""; + private int fallbackValueIndex; + private int valueIndex; + private int valueLineNumber; + private string valueContext = ""; + private SymbolRecord? valueContainer; + private bool hasValueLocation; + + public bool Active { get; private set; } + + public void Start( + string property, + string initialValue, + int initialValueIndex, + string context, + int lineNumber, + SymbolRecord? container) + { + Active = true; + this.property = property; + fallbackValueIndex = initialValueIndex; + value.Clear(); + hasValueLocation = false; + AppendValue(initialValue, initialValueIndex, context, lineNumber, container); + } + + public bool Advance( + string line, + string context, + int lineNumber, + SymbolRecord? container, + out CompletedBindingPropertyElement completed) + { + completed = default; + if (!TryFindBindingPropertyElementEnd(line, property, out var closingTag)) + { + AppendValue(line, 0, context, lineNumber, container); + return false; + } + + AppendValue(line[..closingTag.Index], 0, context, lineNumber, container); + completed = new CompletedBindingPropertyElement( + value.ToString(), + hasValueLocation ? valueIndex : fallbackValueIndex, + hasValueLocation ? valueLineNumber : lineNumber, + hasValueLocation ? valueContext : context, + hasValueLocation ? valueContainer : container); + Reset(); + return true; + } + + private void AppendValue( + string segment, + int segmentIndex, + string context, + int lineNumber, + SymbolRecord? container) + { + var trimmedStart = 0; + while (trimmedStart < segment.Length && char.IsWhiteSpace(segment[trimmedStart])) + trimmedStart++; + var trimmedEnd = segment.Length; + while (trimmedEnd > trimmedStart && char.IsWhiteSpace(segment[trimmedEnd - 1])) + trimmedEnd--; + if (trimmedStart >= trimmedEnd) + return; + + if (!hasValueLocation) + { + valueIndex = segmentIndex + trimmedStart; + valueLineNumber = lineNumber; + valueContext = context; + valueContainer = container; + hasValueLocation = true; + } + + if (value.Length > 0) + value.Append(' '); + value.Append(segment, trimmedStart, trimmedEnd - trimmedStart); + } + + private void Reset() + { + Active = false; + property = ""; + fallbackValueIndex = 0; + valueIndex = 0; + valueLineNumber = 0; + valueContext = ""; + valueContainer = null; + hasValueLocation = false; + value.Clear(); + } + } + + internal readonly record struct CompletedBindingPropertyElement( + string Value, + int ValueIndex, + int LineNumber, + string Context, + SymbolRecord? Container); + + internal sealed class BindingMarkupExtensionState + { + private readonly StringBuilder content = new(); + private string kind = ""; + private int braceDepth; + private int fallbackContentIndex; + private int contentIndex; + private int contentLineNumber; + private string contentContext = ""; + private SymbolRecord? contentContainer; + private bool hasContentLocation; + + public bool Active { get; private set; } + + public void Start( + string kind, + string initialContent, + int initialContentIndex, + int initialBraceDepth, + string context, + int lineNumber, + SymbolRecord? container) + { + Active = true; + this.kind = kind; + braceDepth = initialBraceDepth; + fallbackContentIndex = initialContentIndex; + content.Clear(); + hasContentLocation = false; + AppendContent(initialContent, initialContentIndex, context, lineNumber, container); + } + + public bool Advance( + string line, + string context, + int lineNumber, + SymbolRecord? container, + out CompletedBindingMarkupExtension completed) + { + completed = default; + for (var i = 0; i < line.Length; i++) + { + if (line[i] == '{') + { + braceDepth++; + continue; + } + + if (line[i] != '}') + continue; + + braceDepth--; + if (braceDepth != 0) + continue; + + AppendContent(line[..i], 0, context, lineNumber, container); + completed = new CompletedBindingMarkupExtension( + kind, + content.ToString(), + hasContentLocation ? contentIndex : fallbackContentIndex, + hasContentLocation ? contentLineNumber : lineNumber, + hasContentLocation ? contentContext : context, + hasContentLocation ? contentContainer : container, + i + 1 < line.Length ? new string(' ', i + 1) + line[(i + 1)..] : ""); + Reset(); + return true; + } + + AppendContent(line, 0, context, lineNumber, container); + return false; + } + + private void AppendContent( + string segment, + int segmentIndex, + string context, + int lineNumber, + SymbolRecord? container) + { + var trimmedStart = 0; + while (trimmedStart < segment.Length && char.IsWhiteSpace(segment[trimmedStart])) + trimmedStart++; + var trimmedEnd = segment.Length; + while (trimmedEnd > trimmedStart && char.IsWhiteSpace(segment[trimmedEnd - 1])) + trimmedEnd--; + if (trimmedStart >= trimmedEnd) + return; + + if (!hasContentLocation) + { + contentIndex = segmentIndex + trimmedStart; + contentLineNumber = lineNumber; + contentContext = context; + contentContainer = container; + hasContentLocation = true; + } + + if (content.Length > 0) + content.Append(' '); + content.Append(segment, trimmedStart, trimmedEnd - trimmedStart); + } + + private void Reset() + { + Active = false; + kind = ""; + braceDepth = 0; + fallbackContentIndex = 0; + contentIndex = 0; + contentLineNumber = 0; + contentContext = ""; + contentContainer = null; + hasContentLocation = false; + content.Clear(); + } + } + + internal readonly record struct CompletedBindingMarkupExtension( + string Kind, + string Content, + int ContentIndex, + int LineNumber, + string Context, + SymbolRecord? Container, + string RemainingLine); + + private static void AddReference( + List references, + HashSet seen, + long fileId, + string name, + int nameIndex, + string kind, + string context, + int lineNumber, + SymbolRecord? container) + => ReferenceExtractor.AddReference(references, seen, fileId, name, nameIndex, kind, context, lineNumber, container, "xml"); + + private static IEnumerable NormalizeXamlTypeValues(string value) + { + var normalized = NormalizeXamlMarkupArgument(value); + if (normalized.Length > 0) + yield return normalized; + } + + private static IEnumerable NormalizeXamlTypeArguments(string value) + { + foreach (var segment in value.Split(new[] { ',', '(', ')' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var normalized = NormalizeXamlMarkupArgument(segment); + if (normalized.Length > 0) + yield return normalized; + } + } + + private static string NormalizeNamedMarkupReference(string content) + { + foreach (var argument in SplitMarkupArguments(content)) + { + var normalized = NormalizeXamlMarkupArgument(argument); + if (normalized.Length > 0) + return normalized; + } + + return ""; + } + + private static IEnumerable NormalizeBindingReferences(string kind, string content) + { + foreach (var argument in SplitMarkupArguments(content)) + { + var trimmed = argument.Trim(); + if (trimmed.Length == 0) + continue; + + var equalsIndex = trimmed.IndexOf('='); + var key = equalsIndex >= 0 ? trimmed[..equalsIndex].Trim() : ""; + var value = equalsIndex >= 0 ? trimmed[(equalsIndex + 1)..].Trim() : trimmed; + if (key.Equals("Source", StringComparison.OrdinalIgnoreCase) + || key.Equals("Converter", StringComparison.OrdinalIgnoreCase) + || key.Equals("ConverterParameter", StringComparison.OrdinalIgnoreCase)) + continue; + + if (key.Length == 0 + || key.Equals("Path", StringComparison.OrdinalIgnoreCase) + || key.Equals("ElementName", StringComparison.OrdinalIgnoreCase) + || (kind.Equals("TemplateBinding", StringComparison.OrdinalIgnoreCase) + && key.Equals("Property", StringComparison.OrdinalIgnoreCase))) + { + var normalized = NormalizeBindingPath(value); + if (normalized.Length > 0) + yield return normalized; + } + } + } + + private static void EmitBindingElementAttributeReferences( + string attributes, + int attributesIndex, + List references, + HashSet seen, + long fileId, + string context, + int lineNumber, + SymbolRecord? container) + { + foreach (Match attribute in XamlAttributeRegex.Matches(attributes)) + { + var name = attribute.Groups["name"].Value; + if (!name.Equals("Path", StringComparison.OrdinalIgnoreCase) + && !name.Equals("ElementName", StringComparison.OrdinalIgnoreCase)) + continue; + + var value = NormalizeBindingPath(attribute.Groups["value"].Value); + if (value.Length > 0) + AddReference(references, seen, fileId, value, attributesIndex + attribute.Groups["value"].Index, "reference", context, lineNumber, container); + } + } + + private static string NormalizeBindingPath(string value) + { + var normalized = NormalizeXamlMarkupArgument(value).Trim(); + if (normalized.Length == 0 || normalized == ".") + return ""; + + var lastDot = normalized.LastIndexOf('.'); + if (lastDot >= 0 && lastDot + 1 < normalized.Length) + normalized = normalized[(lastDot + 1)..]; + + normalized = normalized.Trim(); + return normalized == "." ? "" : normalized; + } + + private static IEnumerable EnumerateBindingMarkupExtensions(string line) + { + return EnumerateMarkupExtensions(line, BindingMarkupKinds); + } + + private static IEnumerable EnumerateResourceMarkupExtensions(string line) + { + return EnumerateMarkupExtensions(line, ResourceMarkupKinds); + } + + private static IEnumerable EnumerateMarkupExtensions(string line, string[] kinds) + { + var cursor = 0; + while (cursor < line.Length) + { + var openBrace = line.IndexOf('{', cursor); + if (openBrace < 0) + yield break; + + var kindStart = openBrace + 1; + while (kindStart < line.Length && char.IsWhiteSpace(line[kindStart])) + kindStart++; + + if (!TryReadMarkupKind(line, kindStart, kinds, out var kind, out var kindEnd)) + { + cursor = openBrace + 1; + continue; + } + + var depth = 1; + for (var i = kindEnd; i < line.Length; i++) + { + if (line[i] == '{') + { + depth++; + continue; + } + + if (line[i] != '}') + continue; + + depth--; + if (depth != 0) + continue; + + yield return new BindingMarkupExtension(kind, line[kindEnd..i], kindEnd); + cursor = i + 1; + goto nextBinding; + } + + yield break; + + nextBinding: + continue; + } + } + + private static bool TryFindUnclosedBindingMarkupExtension(string line, out BindingMarkupExtensionStart start) + { + var cursor = 0; + while (cursor < line.Length) + { + var openBrace = line.IndexOf('{', cursor); + if (openBrace < 0) + break; + + var kindStart = openBrace + 1; + while (kindStart < line.Length && char.IsWhiteSpace(line[kindStart])) + kindStart++; + + if (!TryReadMarkupKind(line, kindStart, BindingMarkupKinds, out var kind, out var kindEnd)) + { + cursor = openBrace + 1; + continue; + } + + var depth = 1; + for (var i = kindEnd; i < line.Length; i++) + { + if (line[i] == '{') + { + depth++; + continue; + } + + if (line[i] != '}') + continue; + + depth--; + if (depth == 0) + { + cursor = i + 1; + goto nextBinding; + } + } + + start = new BindingMarkupExtensionStart(kind, line[kindEnd..], kindEnd, depth); + return true; + + nextBinding: + continue; + } + + start = default; + return false; + } + + private static bool TryReadMarkupKind(string line, int kindStart, string[] kinds, out string kind, out int kindEnd) + { + foreach (var candidate in kinds) + { + if (kindStart + candidate.Length > line.Length) + continue; + if (!line.AsSpan(kindStart, candidate.Length).Equals(candidate.AsSpan(), StringComparison.Ordinal)) + continue; + kindEnd = kindStart + candidate.Length; + if (kindEnd < line.Length && !IsBindingMarkupKindBoundary(line[kindEnd])) + continue; + + kind = candidate; + return true; + } + + kind = ""; + kindEnd = kindStart; + return false; + } + + private static bool IsBindingMarkupKindBoundary(char ch) + => char.IsWhiteSpace(ch) || ch is ',' or '}'; + + private static readonly string[] BindingMarkupKinds = + [ + "TemplateBinding", + "CompiledBinding", + "ReflectionBinding", + "Binding", + "x:Bind", + ]; + + private static readonly string[] ResourceMarkupKinds = + [ + "StaticResourceExtension", + "DynamicResourceExtension", + "StaticResource", + "DynamicResource", + ]; + + private readonly record struct BindingMarkupExtension(string Kind, string Content, int ContentIndex); + private readonly record struct BindingMarkupExtensionStart(string Kind, string InitialContent, int InitialContentIndex, int InitialBraceDepth); + + private static string NormalizeXamlMarkupArgument(string value) + { + value = value.Trim().Trim('"', '\''); + if (value.Length == 0) + return ""; + + if (value.StartsWith("ResourceKey=", StringComparison.Ordinal)) + value = value["ResourceKey=".Length..].Trim(); + if (TryNormalizeXStaticMarkup(value, out var staticMember)) + return staticMember; + + var equalsIndex = value.IndexOf('='); + if (equalsIndex >= 0) + value = value[(equalsIndex + 1)..].Trim(); + + if (TryNormalizeXStaticMarkup(value, out staticMember)) + return staticMember; + + if (value.StartsWith("{x:Type ", StringComparison.Ordinal)) + value = value["{x:Type ".Length..].TrimEnd('}', ' '); + else if (value.StartsWith("{x:TypeExtension ", StringComparison.Ordinal)) + value = value["{x:TypeExtension ".Length..].TrimEnd('}', ' '); + else if (value.StartsWith("{x:Static ", StringComparison.Ordinal)) + value = value["{x:Static ".Length..].TrimEnd('}', ' '); + + if (value.StartsWith("TypeName=", StringComparison.Ordinal)) + value = value["TypeName=".Length..].Trim(); + if (value.StartsWith("Member=", StringComparison.Ordinal)) + value = value["Member=".Length..].Trim(); + if (value.StartsWith("Name=", StringComparison.Ordinal)) + value = value["Name=".Length..].Trim(); + if (value.StartsWith("ResourceKey=", StringComparison.Ordinal)) + value = value["ResourceKey=".Length..].Trim(); + + var memberTypeEnd = value.IndexOf("}.", StringComparison.Ordinal); + if (memberTypeEnd >= 0 && memberTypeEnd + 2 < value.Length) + { + var typeExpression = value[..(memberTypeEnd + 1)].Trim(); + var typeName = NormalizeXamlMarkupArgument(typeExpression); + var memberName = value[(memberTypeEnd + 2)..].Trim(); + if (typeName.Length > 0 && memberName.Length > 0) + return $"{typeName}.{memberName}"; + } + + if (value.StartsWith("{x:Type ", StringComparison.Ordinal)) + value = value["{x:Type ".Length..].TrimEnd('}', ' '); + if (value.StartsWith("{x:TypeExtension ", StringComparison.Ordinal)) + value = value["{x:TypeExtension ".Length..].TrimEnd('}', ' '); + + return value.Trim().TrimEnd('}', '/', '>'); + } + + private static bool TryNormalizeXStaticMarkup(string value, out string normalized) + { + normalized = ""; + if (!value.StartsWith("{x:Static ", StringComparison.Ordinal)) + return false; + + value = value["{x:Static ".Length..].TrimEnd('}', ' '); + if (value.StartsWith("Member=", StringComparison.Ordinal)) + value = value["Member=".Length..].Trim(); + + var memberTypeEnd = value.IndexOf("}.", StringComparison.Ordinal); + if (memberTypeEnd >= 0 && memberTypeEnd + 2 < value.Length) + { + var typeExpression = value[..(memberTypeEnd + 1)].Trim(); + var typeName = NormalizeXamlMarkupArgument(typeExpression); + var memberName = value[(memberTypeEnd + 2)..].Trim().TrimEnd('}'); + if (typeName.Length > 0 && memberName.Length > 0) + { + normalized = $"{typeName}.{memberName}"; + return true; + } + } + + normalized = value.Trim().TrimEnd('}'); + return normalized.Length > 0; + } + + private static IEnumerable SplitMarkupArguments(string content) + { + var start = 0; + var depth = 0; + char quote = '\0'; + for (var i = 0; i <= content.Length; i++) + { + if (i < content.Length) + { + var ch = content[i]; + if (quote != '\0') + { + if (ch == quote && (i == 0 || content[i - 1] != '\\')) + quote = '\0'; + continue; + } + + if (ch is '\'' or '"') + { + quote = ch; + continue; + } + + if (ch == '{') + { + depth++; + continue; + } + + if (ch == '}' && depth > 0) + { + depth--; + continue; + } + + if (ch != ',' || depth != 0) + continue; + } + + var segment = content[start..i].Trim(); + if (segment.Length > 0) + yield return segment; + start = i + 1; + } + } +} diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs index 6e6350edb5..e5b1146e30 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs @@ -26,6 +26,7 @@ internal static List ExtractCore(ReferenceExtractionContext req content = preparedInput.Content; var lines = preparedInput.Lines; + var xamlReferenceEnabled = language == "xml" && XamlReferenceExtractor.IsXaml(lines); var structuralLines = preparedInput.StructuralLines; var csharpLinesInsideMultilineStringContent = preparedInput.CSharpLinesInsideMultilineStringContent; var csharpLinesInsideBlockComment = preparedInput.CSharpLinesInsideBlockComment; @@ -63,6 +64,7 @@ internal static List ExtractCore(ReferenceExtractionContext req var csharpAttrTopLevelRanges = csharpAttrTables.Item2; var definitionNamesComparer = GetDefinitionNamesComparer(language); var definitionNamesByLine = BuildDefinitionNamesByLine(language, symbols); + var allDefinitionNames = BuildAllDefinitionNames(language, symbols); var fileDefinitionNames = isRazorFile ? new HashSet(symbols.Select(symbol => symbol.Name), StringComparer.Ordinal) : null; @@ -125,6 +127,9 @@ internal static List ExtractCore(ReferenceExtractionContext req var kotlinInfixFunctionNames = KotlinReferenceExtractor.BuildInfixFunctionNames(language, symbols); KotlinReferenceExtractor.AddDeclaredInfixFunctionNames(language, lines, kotlinInfixFunctionNames); var callableDefinitionNames = BuildCallableDefinitionNames(language, symbols); + var stylusVariableDefinitionNames = language == "stylus" + ? CssReferenceExtractor.BuildStylusVariableDefinitionNames(lines) + : null; var dockerfileStageNames = DockerfileReferenceExtractor.BuildStageNames(language, symbols); var dockerfileVariableNames = DockerfileReferenceExtractor.BuildVariableNames(language, symbols); var shellCallableNames = ShellReferenceExtractor.BuildCallableNames(language, symbols); @@ -202,8 +207,23 @@ bool HasActiveSameFileCSharpTypeCandidate(string typeExpression, int lineNumber) var markupSchemaState = language is "graphql" or "html" or "markdown" ? new MarkupSchemaReferenceExtractor.MarkupState() : null; + var xamlInXmlComment = false; + var xamlBindingPropertyElementState = language == "xml" + ? new XamlReferenceExtractor.BindingPropertyElementState() + : null; + var xamlBindingMarkupExtensionState = language == "xml" + ? new XamlReferenceExtractor.BindingMarkupExtensionState() + : null; SymbolRecord? phpDocblockContainer = null; HashSet? phpDocblockPropertyNames = null; + var sassPreparedCommentState = language == "sass" + ? new CssReferenceExtractor.SassLoudCommentState() + : null; + var sassOriginalCommentState = language == "sass" + ? new CssReferenceExtractor.SassLoudCommentState() + : null; + var sassStylusPreparedInBlockComment = false; + var sassStylusOriginalInBlockComment = false; for (int i = 0; i < lines.Length; i++) { @@ -213,6 +233,21 @@ bool HasActiveSameFileCSharpTypeCandidate(string typeExpression, int lineNumber) var lineNumber = i + 1; var originalLine = lines[i]; var preparedLine = luaPreparedLines?[i] ?? lispReferenceLines?[i] ?? preparedLines[i]; + var originalLineForLanguage = originalLine; + if (language == "sass") + { + preparedLine = CssReferenceExtractor.MaskSassBlockCommentLine(preparedLine, sassPreparedCommentState!); + originalLineForLanguage = CssReferenceExtractor.MaskSassBlockCommentLine(originalLine, sassOriginalCommentState!); + } + else if (language == "stylus") + { + preparedLine = CssReferenceExtractor.MaskSassStylusBlockCommentLine( + preparedLine, + ref sassStylusPreparedInBlockComment); + originalLineForLanguage = CssReferenceExtractor.MaskSassStylusBlockCommentLine( + originalLine, + ref sassStylusOriginalInBlockComment); + } var csharpAttrRangesOnLine = csharpAttrRanges?[i]; var csharpAttrTopLevelOnLine = csharpAttrTopLevelRanges?[i]; SymbolRecord? phpLineContainer = null; @@ -946,6 +981,26 @@ bool ShouldSuppressDefinitionCall(string resolvedName, int callIndex) definitionNames, container); } + else if (language == "sass") + { + CssReferenceExtractor.EmitSass(preparedLine, originalLineForLanguage, references, seen, fileId, context, lineNumber, container); + continue; + } + else if (language == "stylus") + { + CssReferenceExtractor.EmitStylus(preparedLine, originalLineForLanguage, references, seen, fileId, context, lineNumber, allDefinitionNames, stylusVariableDefinitionNames, container); + continue; + } + else if (language == "xml" && xamlReferenceEnabled) + { + var xamlLine = XamlReferenceExtractor.StripXmlComments(originalLine, ref xamlInXmlComment); + XamlReferenceExtractor.Emit(xamlLine, context, lineNumber, references, seen, fileId, container, xamlBindingPropertyElementState!, xamlBindingMarkupExtensionState!); + continue; + } + else if (language == "xml") + { + continue; + } if (language == "terraform") { diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs index 9972e28f05..7ca43204e5 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs @@ -31,7 +31,7 @@ internal sealed class CSharpWhereConstraintState "assembly", "r", "powershell", "shell", "haskell", "solidity", "gradle", "terraform", "protobuf", "dockerfile", "makefile", "cmake", "justfile", "msbuild", - "zig", "css", "graphql", "html", "markdown", "fortran", "pascal", "objc", "smalltalk" + "zig", "css", "sass", "stylus", "xml", "graphql", "html", "markdown", "fortran", "pascal", "objc", "smalltalk" ]; private static readonly IReadOnlyDictionary Extractors = diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs index b466714318..927fe31986 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs @@ -304,6 +304,18 @@ private static bool IsFunctionLikeSymbolKind(string kind) { "all", "clean", "install", "build", "run", "help", }, + // Sass/Stylus accept CSS function syntax without separators; keep common CSS built-ins + // from flowing through the shared CallRegex after the language-specific extractors skip them. + // Sass/Stylus は CSS 関数構文を区切りなしで受け付けるため、言語専用 extractor で除外した + // 代表的な CSS built-in を共有 CallRegex 側でも call として残さない。 + ["sass"] = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "url", "var", "calc", "rgb", "rgba", "hsl", "hsla", + }, + ["stylus"] = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "url", "var", "calc", "rgb", "rgba", "hsl", "hsla", + }, }; private static readonly Dictionary> LanguageSpecificCallNameKeeps = new(StringComparer.Ordinal) { @@ -945,6 +957,21 @@ private static Dictionary> BuildDefinitionNamesByLine( }); } + private static HashSet BuildAllDefinitionNames( + string language, + IReadOnlyList symbols) + { + var names = new HashSet(GetDefinitionNamesComparer(language)); + foreach (var symbol in symbols) + { + names.Add(symbol.Name); + if (language == "sql") + SqlReferenceExtractor.AddDefinitionNameAliases(names, symbol); + } + + return names; + } + private static StringComparer GetDefinitionNamesComparer(string language) => language == "sql" ? StringComparer.OrdinalIgnoreCase diff --git a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs index 9f44033658..ee1e81193b 100644 --- a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs +++ b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs @@ -217,10 +217,10 @@ internal readonly record struct ProjectMarkerFingerprintResult(string? Fingerpri [".less"] = "css", // Less preprocessor / Less プリプロセッサ [".pcss"] = "css", // PostCSS / PostCSS // Sass indented syntax / Stylus use indentation instead of braces, so they live in - // separate search-only buckets — the CSS symbol extractor's brace-based patterns do - // not apply, but exact-name search still works. + // separate buckets with conservative line-level extractors rather than the CSS + // brace-scoped extractor. // Sass インデント構文と Stylus は波括弧ではなくインデントで構造化するため、 - // CSS のシンボル抽出(波括弧ベース)は使わず、検索用の別バケットに分ける。 + // CSS の波括弧スコープ抽出ではなく、保守的な行単位抽出を持つ別バケットに分ける。 [".sass"] = "sass", [".styl"] = "stylus", [".vue"] = "vue", diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs index 1a7bd8f375..e18999d800 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs @@ -680,20 +680,7 @@ private static string NormalizeMarkdownAnchorTarget(string target) private static List ExtractXmlSymbols(long fileId, string[] lines) { - var hasXamlNamespace = false; - foreach (var line in lines) - { - if (line.IndexOf("xmlns:x=", StringComparison.OrdinalIgnoreCase) < 0) - continue; - if (line.IndexOf("schemas.microsoft.com/winfx/2006/xaml", StringComparison.OrdinalIgnoreCase) >= 0 - || line.IndexOf("github.com/avaloniaui", StringComparison.OrdinalIgnoreCase) >= 0) - { - hasXamlNamespace = true; - break; - } - } - - if (!hasXamlNamespace) + if (!XamlReferenceExtractor.IsXaml(lines)) return []; var rawText = string.Join('\n', lines); diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs index c48670d3c2..0bcf26ec0d 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs @@ -18,6 +18,7 @@ public static partial class SymbolExtractor public const int ExpandedLanguageContractVersion = 2; public const int CSharpContractVersion = 2; public const int DockerfileContractVersion = 2; + public const int StyleAndXamlContractVersion = 2; private static readonly Regex GraphQLInputBlockRegex = new( @"^\s*(?:extend\s+)?input\s+(?\w+)[^{]*\{(?.*?)^\s*\}", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Multiline | RegexOptions.Singleline); @@ -53,6 +54,7 @@ public static int GetContractVersion(string? lang) null or "" => DefaultContractVersion, "csharp" => CSharpContractVersion, "dockerfile" => DockerfileContractVersion, + "sass" or "stylus" or "xml" => StyleAndXamlContractVersion, "cmake" or "graphql" or "html" or "json" or "justfile" or "markdown" or "msbuild" or "yaml" => ExpandedLanguageContractVersion, _ => DefaultContractVersion, }; @@ -2032,6 +2034,29 @@ private enum JavaScriptTypeScriptFunctionHeaderConsumeResult // SCSS placeholder selector / SCSS プレースホルダーセレクタ new("class", new Regex(@"^\s*(?%[\w-]+)\s*[,{]", RegexOptions.Compiled), BodyStyle.Brace), ], + ["sass"] = + [ + // Sass indented syntax has no braces, so keep these as line-level anchors. + // Sass インデント構文は波括弧を持たないため、行単位のアンカーとして扱う。 + new("import", new Regex(@"^\s*@(?:import|use|forward)\s+(?.+?)(?:\s*!default)?\s*$", RegexOptions.Compiled), BodyStyle.None), + new("function", new Regex(@"^\s*(?:@mixin\s+|=)(?[\w-]+)", RegexOptions.Compiled), BodyStyle.None), + new("function", new Regex(@"^\s*@function\s+(?[\w-]+)", RegexOptions.Compiled), BodyStyle.None), + new("function", new Regex(@"^\s*@keyframes\s+(?[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant), BodyStyle.None), + new("property", new Regex(@"^\s*\$(?[\w-]+)\s*:", RegexOptions.Compiled), BodyStyle.None), + new("class", new Regex(@"^\s*(?[.#%][\w-]+)(?=[\s\.,:>+~\[]|$)", RegexOptions.Compiled), BodyStyle.None), + new("property", new Regex(@"^\s*&(?:(?::(?[\w-]+))|(?:\s*(?:[>+~]\s*)?(?:\.|#)?(?[\w-]+)))", RegexOptions.Compiled), BodyStyle.None), + ], + ["stylus"] = + [ + // Stylus supports optional punctuation, so only capture conservative declaration shapes. + // Stylus は句読点を省略できるため、保守的な宣言形だけを捕捉する。 + new("import", new Regex(@"^\s*@(?:import|require|use)\s+(?.+?)\s*$", RegexOptions.Compiled), BodyStyle.None), + new("function", new Regex(@"^(?[A-Za-z_][\w-]*)\s*\([^)\r\n]*\)\s*$", RegexOptions.Compiled), BodyStyle.None), + new("function", new Regex(@"^\s*@keyframes\s+(?[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant), BodyStyle.None), + new("property", new Regex(@"^\s*\$?(?[A-Za-z_][\w-]*)\s*(?:=|:=)\s*", RegexOptions.Compiled), BodyStyle.None), + new("class", new Regex(@"^\s*(?[.#%][\w-]+)(?=[\s\.,:>+~\[]|$)", RegexOptions.Compiled), BodyStyle.None), + new("property", new Regex(@"^\s*&(?:(?::(?[\w-]+))|(?:\s*(?:[>+~]\s*)?(?:\.|#)?(?[\w-]+)))", RegexOptions.Compiled), BodyStyle.None), + ], // HTML does not use the regex pattern loop — it needs true tag-structure // awareness (attribute enumeration, quoted-value handling, custom-element // detection) that regex alone can't express without losing outer-tag @@ -2144,7 +2169,7 @@ private enum JavaScriptTypeScriptFunctionHeaderConsumeResult /// public static IReadOnlyCollection GetSupportedLanguages() => PatternCache.Keys - .Concat(new[] { "commonlisp", "racket", "vue", "svelte", "markdown", "json", "yaml", "razor", "blazor", "cshtml", "solidity" }) + .Concat(new[] { "commonlisp", "racket", "vue", "svelte", "markdown", "json", "yaml", "xml", "razor", "blazor", "cshtml", "solidity" }) .Concat(ExtractorPluginRegistry.SymbolLanguages) .Distinct(StringComparer.Ordinal) .ToArray(); @@ -2362,6 +2387,9 @@ public static List Extract(long fileId, string? lang, string conte var cssScannerLines = lang == "css" ? MaskCssScannerLines(lines) : null; + var sassStylusScannerLines = lang is "sass" or "stylus" + ? MaskSassStylusBlockCommentLines(lang, lines) + : null; var shellScannerLines = lang == "shell" ? MaskShellHeredocLines(lines) : null; @@ -2439,6 +2467,7 @@ public static List Extract(long fileId, string? lang, string conte var structuralLine = structuralLines[i]; var cssScannerLine = cssScannerLines?[i]; + var sassStylusScannerLine = sassStylusScannerLines?[i]; var shellScannerLine = shellScannerLines?[i]; var matchLine = structuralLine; if (lang == "css" && cssScannerLine != null) @@ -2450,6 +2479,10 @@ public static List Extract(long fileId, string? lang, string conte // 保持する。brace/depth 判定だけ別の scanner line を使う。 matchLine = line; } + else if (lang is "sass" or "stylus" && sassStylusScannerLine != null) + { + matchLine = sassStylusScannerLine; + } else if (lang == "shell" && shellScannerLine != null) { matchLine = shellScannerLine; @@ -8193,6 +8226,23 @@ private static bool ShouldSkipJavaScriptTypeScriptStyledFactoryCandidate( return !tagHeadConsumed; } + private static string[] MaskSassStylusBlockCommentLines(string language, string[] originalLines) + { + var maskedLines = new string[originalLines.Length]; + if (language == "sass") + { + var state = new CssReferenceExtractor.SassLoudCommentState(); + for (var i = 0; i < originalLines.Length; i++) + maskedLines[i] = CssReferenceExtractor.MaskSassBlockCommentLine(originalLines[i], state); + return maskedLines; + } + + var inBlockComment = false; + for (var i = 0; i < originalLines.Length; i++) + maskedLines[i] = CssReferenceExtractor.MaskSassStylusBlockCommentLine(originalLines[i], ref inBlockComment); + return maskedLines; + } + private static bool IsJsTsStyledTagHeadBreakingOperator(char c) => c switch { '+' or '-' or '*' or '%' or '?' or '!' or '&' or '|' or '^' or '=' or ',' or ':' or '<' or '>' or '/' => true, diff --git a/tests/CodeIndex.Tests/FileIndexerTests.cs b/tests/CodeIndex.Tests/FileIndexerTests.cs index 064ce5ec34..cd21c01ca7 100644 --- a/tests/CodeIndex.Tests/FileIndexerTests.cs +++ b/tests/CodeIndex.Tests/FileIndexerTests.cs @@ -1289,8 +1289,8 @@ public void GetLanguageExtensions_ExposesPrefixAndFileNameVariants() Assert.Equal("makefile", map["Makefile."]); Assert.Equal("makefile", map["GNUmakefile."]); - // Sass / Stylus are distinct buckets now (indented syntax is incompatible with the CSS extractor). - // Sass / Stylus は別バケット(インデント構文が CSS のシンボル抽出と非互換のため)。 + // Sass / Stylus are distinct buckets with indentation-aware conservative extraction. + // Sass / Stylus はインデント構文向けの保守的な抽出を持つ別バケット。 Assert.Equal("sass", map[".sass"]); Assert.Equal("stylus", map[".styl"]); Assert.Equal("css", map[".scss"]); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index b59208d47b..dd96ccdba8 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -2267,14 +2267,12 @@ public void RunLanguages_JsonListsCSharpRazorAliases() [Fact] public void RunLanguages_Json_SearchOnlyBucketsAdvertiseZeroSymbolAndGraphSupport() { - // Search-only languages that intentionally live outside the Python / CSS extractors - // (Cython .pyx/.pxd, Sass .sass, Stylus .styl, and the newly added extension-only - // languages) must advertise + // Search-only languages that intentionally live outside richer extractors + // (Cython .pyx/.pxd and the newly added extension-only languages) must advertise // symbol_extraction=false / graph_queries=false so AI clients can tell the difference // between "indexed with symbols" and "indexed for search only". - // 意図的に Python / CSS 抽出器の対象外になっている search-only 言語 - // (Cython の .pyx/.pxd、Sass の .sass、Stylus の .styl、そして新規追加の - // 拡張子ベース言語)は、 + // 意図的に richer な抽出器の対象外になっている search-only 言語 + // (Cython の .pyx/.pxd と新規追加の拡張子ベース言語)は、 // symbol_extraction=false / graph_queries=false で広告しなければならない。 // こうしないと、AI クライアントが「シンボル付きインデックス」と「検索のみインデックス」を区別できない。 var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages(["--json"], _jsonOptions)); @@ -2285,7 +2283,7 @@ public void RunLanguages_Json_SearchOnlyBucketsAdvertiseZeroSymbolAndGraphSuppor var languages = document.RootElement.GetProperty("languages").EnumerateArray() .ToDictionary(entry => entry.GetProperty("lang").GetString()!, entry => entry); - foreach (var searchOnly in new[] { "cython", "sass", "stylus" }) + foreach (var searchOnly in new[] { "cython" }) { Assert.True(languages.ContainsKey(searchOnly), $"expected '{searchOnly}' to be listed"); var entry = languages[searchOnly]; @@ -2319,6 +2317,25 @@ public void RunLanguages_Json_SearchOnlyBucketsAdvertiseZeroSymbolAndGraphSuppor Assert.True(languages["json"].GetProperty("symbol_extraction").GetBoolean(), "json must advertise symbol_extraction=true"); + foreach (var stylesheetPreprocessor in new[] { "sass", "stylus" }) + { + Assert.True(languages.ContainsKey(stylesheetPreprocessor), $"expected '{stylesheetPreprocessor}' to be listed"); + Assert.True(languages[stylesheetPreprocessor].GetProperty("symbol_extraction").GetBoolean(), + $"{stylesheetPreprocessor} must advertise symbol_extraction=true"); + Assert.True(languages[stylesheetPreprocessor].GetProperty("reference_extraction").GetBoolean(), + $"{stylesheetPreprocessor} must advertise reference_extraction=true"); + Assert.True(languages[stylesheetPreprocessor].GetProperty("graph_queries").GetBoolean(), + $"{stylesheetPreprocessor} must advertise graph_queries=true"); + Assert.Empty(languages[stylesheetPreprocessor].GetProperty("capability_gaps").EnumerateArray()); + } + + Assert.True(languages["xml"].GetProperty("symbol_extraction").GetBoolean(), + "xml must advertise symbol_extraction=true for XAML/AXAML files"); + Assert.True(languages["xml"].GetProperty("reference_extraction").GetBoolean(), + "xml must advertise reference_extraction=true for XAML/AXAML files"); + Assert.True(languages["xml"].GetProperty("graph_queries").GetBoolean(), + "xml must advertise graph_queries=true for XAML/AXAML files"); + Assert.True(languages.ContainsKey("solidity"), "expected 'solidity' to be listed"); Assert.True(languages["solidity"].GetProperty("symbol_extraction").GetBoolean(), "solidity must advertise symbol_extraction=true"); diff --git a/tests/CodeIndex.Tests/ReferenceExtractorCssTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorCssTests.cs index 2df2050e97..3e30ecd898 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorCssTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorCssTests.cs @@ -412,4 +412,238 @@ @mixin elevated($depth) { reference.SymbolName == "elevated" && reference.ReferenceKind == "call")); } + + [Fact] + public void Extract_Sass_IndentedVariablesMixinsAndImports_AreReferenced() + { + const string content = """ + /* Loud comment + @use "loud-comment-theme" + +loud-commented(4px) + // Silent comment + @use "silent-comment-theme" + +silent-commented(4px) + @use "theme" + @import url("reset.css") + $primary: #3366cc + $spacing-base: 8px + $accent: #ffcc00 + /* + @use "comment-block-theme" + +commented-rounded(4px) + */ + + =rounded($radius) + border-radius: $radius + =rounded-button($button-radius) + border-radius: $button-radius + + @function spacing-unit($step) + @return $step * 4px + + .button + content: '@use "string-theme"' + color: $primary + color: red // @use "comment-theme" + background: url("logo.png") + border-color: rgb(0, 0, 0) + background: url(http://cdn/a.png) $accent + padding: $spacing-base * 2 + margin: spacing-unit(2) + +rounded(4px) + +rounded-button(4px) + .card:not(.disabled) + display: block + :is(.button, .link) + display: block + """; + + var symbols = SymbolExtractor.Extract(1, "sass", content); + var references = ReferenceExtractor.Extract(1, "sass", content, symbols); + + Assert.Single(references.Where(reference => + reference.SymbolName == "theme" + && reference.ReferenceKind == "import")); + Assert.Single(references.Where(reference => + reference.SymbolName == "reset.css" + && reference.ReferenceKind == "import")); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "string-theme" + && reference.ReferenceKind == "import"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "comment-theme" + && reference.ReferenceKind == "import"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "comment-block-theme" + && reference.ReferenceKind == "import"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "loud-comment-theme" + && reference.ReferenceKind == "import"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "silent-comment-theme" + && reference.ReferenceKind == "import"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "commented-rounded" + && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "loud-commented" + && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "silent-commented" + && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "url(" + && reference.ReferenceKind == "import"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "url" + && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "rgb" + && reference.ReferenceKind == "call"); + Assert.Single(references.Where(reference => + reference.SymbolName == "primary" + && reference.ReferenceKind == "call")); + Assert.Single(references.Where(reference => + reference.SymbolName == "spacing-base" + && reference.ReferenceKind == "call")); + Assert.Single(references.Where(reference => + reference.SymbolName == "accent" + && reference.ReferenceKind == "call")); + Assert.Single(references.Where(reference => + reference.SymbolName == "radius" + && reference.ReferenceKind == "call")); + Assert.Single(references.Where(reference => + reference.SymbolName == "rounded" + && reference.ReferenceKind == "call")); + Assert.Single(references.Where(reference => + reference.SymbolName == "rounded-button" + && reference.ReferenceKind == "call")); + Assert.Single(references.Where(reference => + reference.SymbolName == "spacing-unit" + && reference.ReferenceKind == "call")); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "button" + && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "unit" + && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "not" + && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "is" + && reference.ReferenceKind == "call"); + } + + [Fact] + public void Extract_Stylus_VariablesAndFunctionCalls_AreReferenced() + { + const string content = """ + @require "theme" + @import url("tokens.css") + primary = #3366cc + $accent = #ffcc00 + /* + @require "comment-block-theme" + commentedRounded(8px) + */ + + rounded(radius) + border-radius radius + rounded-button(radius) + border-radius radius + spacing-unit(step) + margin step + + @keyframes fade + from + opacity 0 + + // $commentedAccent + // rounded(8px) + .card:not(.disabled) + display block + :is(.button, .link) + display block + .button + &:hover + color primary + content: "@require 'string-theme'" + color primary + color red // @require "comment-theme" + background $accent + background url(http://cdn/a.png) $accent + background-image url("logo.png") + border-color rgb(0, 0, 0) + rounded(4px) + rounded-button(4px) + spacing-unit(2) + """; + + var symbols = SymbolExtractor.Extract(1, "stylus", content); + var references = ReferenceExtractor.Extract(1, "stylus", content, symbols); + + Assert.Single(references.Where(reference => + reference.SymbolName == "theme" + && reference.ReferenceKind == "import")); + Assert.Single(references.Where(reference => + reference.SymbolName == "tokens.css" + && reference.ReferenceKind == "import")); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "string-theme" + && reference.ReferenceKind == "import"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "comment-theme" + && reference.ReferenceKind == "import"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "comment-block-theme" + && reference.ReferenceKind == "import"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "url(" + && reference.ReferenceKind == "import"); + Assert.Equal(2, references.Count(reference => + reference.SymbolName == "primary" + && reference.ReferenceKind == "call")); + Assert.Equal(2, references.Count(reference => + reference.SymbolName == "accent" + && reference.ReferenceKind == "call")); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "commentedAccent"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "commentedRounded" + && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "fade" + && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "hover" + && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "url" + && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "rgb" + && reference.ReferenceKind == "call"); + Assert.Single(references.Where(reference => + reference.SymbolName == "rounded" + && reference.ReferenceKind == "call")); + Assert.Single(references.Where(reference => + reference.SymbolName == "rounded-button" + && reference.ReferenceKind == "call")); + Assert.Single(references.Where(reference => + reference.SymbolName == "spacing-unit" + && reference.ReferenceKind == "call")); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "button" + && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "unit" + && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "not" + && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "is" + && reference.ReferenceKind == "call"); + } } diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 711b578a0a..39b68c0849 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -572,6 +572,205 @@ public void RegisteredLanguages_MatchSupportedReferenceLanguages() Assert.Equal("csharp", razorExtractor.Language); } + [Fact] + public void Extract_Xml_XamlReferences_CodeBehindResourcesBindingsAndHandlers() + { + const string content = """ + + + +