diff --git a/changelog.d/unreleased/1976.fixed.md b/changelog.d/unreleased/1976.fixed.md new file mode 100644 index 0000000000..1ca02c629e --- /dev/null +++ b/changelog.d/unreleased/1976.fixed.md @@ -0,0 +1,18 @@ +--- +category: fixed +issues: + - 1976 +affected: + - src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs + - src/CodeIndex/Indexer/References/Languages/TypeScriptReferenceExtractor.cs + - src/CodeIndex/Indexer/References/Languages/SwiftReferenceExtractor.cs + - tests/CodeIndex.Tests/ReferenceExtractorTests.cs +--- + +## English + +- **Type alias heritage references now include the underlying type (#1976)** — TypeScript `type Alias = Target` and Swift `typealias Alias = Target` indirection now emits an additional `type_reference` from alias-mediated heritage uses to the underlying type. + +## 日本語 + +- **type alias 経由の継承参照で underlying type も記録するようになりました (#1976)** — TypeScript の `type Alias = Target` と Swift の `typealias Alias = Target` 経由の継承利用で、alias だけでなく underlying type への追加 `type_reference` も出力します。 diff --git a/src/CodeIndex/Indexer/References/Languages/SwiftReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/SwiftReferenceExtractor.cs index 00cd553eca..0fb91a01ed 100644 --- a/src/CodeIndex/Indexer/References/Languages/SwiftReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/SwiftReferenceExtractor.cs @@ -5,6 +5,16 @@ namespace CodeIndex.Indexer; internal static class SwiftReferenceExtractor { + internal readonly record struct LineRange(int StartLine, int EndLine); + internal readonly record struct TypeAliasBinding( + string Alias, + string Target, + int BindingLine, + int? EndLine, + int BraceDepth, + IReadOnlyList ShadowRanges, + IReadOnlySet TypeParameters); + private static readonly string[] DeclarationKeywords = ["let", "var"]; private static readonly string[] TypeOperatorKeywords = ["is", "as"]; private static readonly Regex PropertyWrapperDeclarationRegex = new( @@ -13,6 +23,12 @@ internal static class SwiftReferenceExtractor private static readonly Regex PropertyWrapperAttributeRegex = new( @"@(?[A-Z]\w*(?:\.[A-Z]\w*)?)", RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex TypeAliasRegex = new( + @"^\s*(?:(?:public|private|internal|open|fileprivate|package)\s+)?typealias\s+(?`[^`]+`|\w+)(?\s*<[^=]+>)?\s*=\s*(?.+)$", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex TypeDeclarationShadowRegex = new( + @"^\s*(?:(?:public|private|internal|open|fileprivate|package)\s+)?(?:final\s+)?(?:class|struct|enum|protocol)\s+(?`[^`]+`|\w+)\b", + RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly HashSet NonWrapperPropertyAttributes = new(StringComparer.Ordinal) { "IBOutlet", @@ -77,6 +93,268 @@ public static void EmitTypePositionReferences( resolveContainerForColumn); } + public static IReadOnlyList BuildTypeAliasTargets(IReadOnlyList preparedLines) + { + var aliases = new List(); + var braceDepths = BuildBraceDepthsBeforeLine(preparedLines); + for (var index = 0; index < preparedLines.Count; index++) + { + var line = preparedLines[index]; + var match = TypeAliasRegex.Match(line); + if (!match.Success) + continue; + + var target = match.Groups["target"].Value.Trim(); + if (target.Length > 0) + aliases.Add(new TypeAliasBinding( + TrimSwiftBackticks(match.Groups["alias"].Value), + target, + index + 1, + FindScopedAliasEndLine(preparedLines, braceDepths, index), + braceDepths[index], + BuildTypeAliasShadowRanges(preparedLines, braceDepths, TrimSwiftBackticks(match.Groups["alias"].Value)), + ExtractGenericTypeParameters(match.Groups["params"].Value))); + } + + return aliases; + } + + public static void EmitAliasTargetReferences( + string preparedLine, + IReadOnlyList aliases, + List references, + HashSet seen, + long fileId, + string context, + int lineNumber, + Func resolveContainerForColumn) + { + if (aliases.Count == 0 || TypeAliasRegex.IsMatch(preparedLine)) + return; + + foreach (var alias in aliases.Select(binding => binding.Alias).Distinct(StringComparer.Ordinal)) + { + var searchStart = 0; + while (searchStart < preparedLine.Length) + { + var index = preparedLine.IndexOf(alias, searchStart, StringComparison.Ordinal); + if (index < 0) + break; + + searchStart = index + alias.Length; + if (!HasIdentifierBoundaries(preparedLine, index, alias.Length)) + continue; + var column = index + 1; + if (!references.Any(reference => + reference.FileId == fileId + && reference.Line == lineNumber + && reference.Column == column + && reference.ReferenceKind == "type_reference" + && string.Equals(reference.SymbolName, alias, StringComparison.Ordinal))) + { + continue; + } + + var binding = FindActiveTypeAliasBinding(aliases, alias, lineNumber); + if (binding is null) + continue; + + TypedLanguageReferenceExtractor.EmitTypeExpressionReferences( + binding.Value.Target, + index, + "swift", + references, + seen, + fileId, + context, + lineNumber, + resolveContainerForColumn(index), + binding.Value.TypeParameters); + } + } + } + + private static TypeAliasBinding? FindActiveTypeAliasBinding( + IReadOnlyList aliases, + string alias, + int lineNumber) + { + TypeAliasBinding? best = null; + foreach (var binding in aliases) + { + if (!string.Equals(binding.Alias, alias, StringComparison.Ordinal) + || lineNumber <= binding.BindingLine + || (binding.EndLine is int endLine && lineNumber > endLine) + || IsInsideScopedShadow(binding.ShadowRanges, lineNumber)) + { + continue; + } + + if (best is null + || binding.BraceDepth > best.Value.BraceDepth + || (binding.BraceDepth == best.Value.BraceDepth && binding.BindingLine > best.Value.BindingLine)) + { + best = binding; + } + } + + return best; + } + + private static IReadOnlyList BuildTypeAliasShadowRanges( + IReadOnlyList preparedLines, + IReadOnlyList braceDepths, + string alias) + { + var ranges = new List(); + for (var index = 0; index < preparedLines.Count; index++) + { + var line = preparedLines[index]; + var typeDeclaration = TypeDeclarationShadowRegex.Match(line); + if (typeDeclaration.Success && string.Equals(TrimSwiftBackticks(typeDeclaration.Groups["name"].Value), alias, StringComparison.Ordinal)) + { + ranges.Add(new LineRange(index + 1, FindScopedAliasEndLine(preparedLines, braceDepths, index) ?? preparedLines.Count)); + continue; + } + + if (DeclaresGenericTypeParameter(line, alias)) + ranges.Add(new LineRange(index + 1, FindScopedAliasEndLine(preparedLines, braceDepths, index) ?? index + 1)); + } + + return ranges; + } + + private static bool DeclaresGenericTypeParameter(string line, string alias) + { + var openAngle = line.IndexOf('<'); + if (openAngle < 0) + return false; + + var closeAngle = line.IndexOf('>', openAngle + 1); + if (closeAngle <= openAngle) + return false; + + var prefix = line[..openAngle]; + if (!ContainsKeyword(prefix, "class") + && !ContainsKeyword(prefix, "struct") + && !ContainsKeyword(prefix, "enum") + && !ContainsKeyword(prefix, "protocol") + && !ContainsKeyword(prefix, "func") + && !ContainsKeyword(prefix, "typealias")) + { + return false; + } + + foreach (var parameter in line.Substring(openAngle + 1, closeAngle - openAngle - 1).Split(',')) + { + var name = parameter.Trim().Split([' ', ':', '='], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + if (string.Equals(TrimSwiftBackticks(name ?? string.Empty), alias, StringComparison.Ordinal)) + return true; + } + + return false; + } + + private static IReadOnlySet ExtractGenericTypeParameters(string parameters) + { + var names = new HashSet(StringComparer.Ordinal); + var openAngle = parameters.IndexOf('<'); + var closeAngle = parameters.LastIndexOf('>'); + if (openAngle < 0 || closeAngle <= openAngle) + return names; + + foreach (var parameter in parameters.Substring(openAngle + 1, closeAngle - openAngle - 1).Split(',')) + { + var name = parameter.Trim().Split([' ', ':', '='], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(name)) + names.Add(TrimSwiftBackticks(name)); + } + + return names; + } + + private static bool ContainsKeyword(string text, string keyword) + { + var searchStart = 0; + while (searchStart < text.Length) + { + var index = text.IndexOf(keyword, searchStart, StringComparison.Ordinal); + if (index < 0) + return false; + + if (HasIdentifierBoundaries(text, index, keyword.Length)) + return true; + + searchStart = index + keyword.Length; + } + + return false; + } + + private static bool IsInsideScopedShadow(IReadOnlyList ranges, int lineNumber) + { + foreach (var range in ranges) + { + if (lineNumber >= range.StartLine && lineNumber <= range.EndLine) + return true; + } + + return false; + } + + private static int? FindScopedAliasEndLine( + IReadOnlyList preparedLines, + IReadOnlyList braceDepths, + int bindingLineIndex) + { + var bindingDepth = braceDepths[bindingLineIndex]; + if (bindingDepth <= 0) + return null; + + for (var index = bindingLineIndex + 1; index < preparedLines.Count; index++) + { + if (braceDepths[index] < bindingDepth) + return index; + } + + return preparedLines.Count; + } + + private static int[] BuildBraceDepthsBeforeLine(IReadOnlyList preparedLines) + { + var depths = new int[preparedLines.Count]; + var depth = 0; + for (var index = 0; index < preparedLines.Count; index++) + { + depths[index] = depth; + foreach (var ch in preparedLines[index]) + { + if (ch == '{') + depth++; + else if (ch == '}' && depth > 0) + depth--; + } + } + + return depths; + } + + private static string TrimSwiftBackticks(string value) => + value.Length >= 2 && value[0] == '`' && value[^1] == '`' + ? value[1..^1] + : value; + + private static bool HasIdentifierBoundaries(string line, int start, int length) + { + var before = start == 0 ? '\0' : line[start - 1]; + var afterIndex = start + length; + var after = afterIndex >= line.Length ? '\0' : line[afterIndex]; + return !IsIdentifierPart(before) && !IsIdentifierPart(after); + } + + private static bool IsIdentifierPart(char c) => + c == '_' || char.IsLetterOrDigit(c); + private static void EmitPropertyWrapperTypeReferences( string preparedLine, List references, diff --git a/src/CodeIndex/Indexer/References/Languages/TypeScriptReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/TypeScriptReferenceExtractor.cs index 39ee9a42f8..4f03601da6 100644 --- a/src/CodeIndex/Indexer/References/Languages/TypeScriptReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/TypeScriptReferenceExtractor.cs @@ -6,6 +6,14 @@ namespace CodeIndex.Indexer; internal static class TypeScriptReferenceExtractor { internal readonly record struct LineRange(int StartLine, int EndLine); + internal readonly record struct TypeAliasBinding( + string Alias, + string Target, + int BindingLine, + int? EndLine, + int BraceDepth, + IReadOnlyList ShadowRanges, + IReadOnlySet TypeParameters); internal sealed record NamespaceAliasBinding( string Alias, string ModuleSpecifier, @@ -28,6 +36,9 @@ internal sealed record NamespaceAliasBinding( private static readonly Regex LocalDeclarationRegex = new( @"^\s*(?:(?:const|let|var)\s+|(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+|(?:export\s+)?(?:abstract\s+)?class\s+|(?:export\s+)?interface\s+|(?:export\s+)?type\s+)(?[A-Za-z_$][\w$]*)\b", RegexOptions.Compiled); + private static readonly Regex TypeDeclarationShadowRegex = new( + @"^\s*(?:export\s+)?(?:abstract\s+)?(?:class|interface|enum)\s+(?[A-Za-z_$][\w$]*)\b", + RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly HashSet MappedTypeClauseIgnoredSegments = new(StringComparer.Ordinal) { "as", @@ -37,6 +48,9 @@ internal sealed record NamespaceAliasBinding( "keyof", "readonly", }; + private static readonly Regex TypeAliasRegex = new( + @"^\s*(?:export\s+)?type\s+(?[A-Za-z_$][\w$]*)(?\s*<[^;]*>)?\s*=\s*(?[^;]+)", + RegexOptions.Compiled | RegexOptions.CultureInvariant); public static IReadOnlyList BuildNamespaceAliasBindings( IReadOnlyList originalLines, @@ -226,6 +240,250 @@ public static void EmitDeclarationTypeReferences( resolveContainerForColumn); } + public static IReadOnlyList BuildTypeAliasTargets(IReadOnlyList preparedLines) + { + var aliases = new List(); + var braceDepths = BuildBraceDepthsBeforeLine(preparedLines); + for (var index = 0; index < preparedLines.Count; index++) + { + var line = preparedLines[index]; + var match = TypeAliasRegex.Match(line); + if (!match.Success) + continue; + + var target = TrimAliasTarget(match.Groups["target"].Value); + if (target.Length > 0) + aliases.Add(new TypeAliasBinding( + match.Groups["alias"].Value, + target, + index + 1, + FindScopedAliasEndLine(preparedLines, braceDepths, index), + braceDepths[index], + BuildTypeAliasShadowRanges(preparedLines, braceDepths, match.Groups["alias"].Value), + ExtractGenericTypeParameters(match.Groups["params"].Value))); + } + + return aliases; + } + + public static void EmitAliasTargetReferences( + string preparedLine, + IReadOnlyList aliases, + List references, + HashSet seen, + long fileId, + string context, + int lineNumber, + Func resolveContainerForColumn) + { + if (aliases.Count == 0 || TypeAliasRegex.IsMatch(preparedLine)) + return; + + foreach (var alias in aliases.Select(binding => binding.Alias).Distinct(StringComparer.Ordinal)) + { + var searchStart = 0; + while (searchStart < preparedLine.Length) + { + var index = preparedLine.IndexOf(alias, searchStart, StringComparison.Ordinal); + if (index < 0) + break; + + searchStart = index + alias.Length; + if (!HasIdentifierBoundaries(preparedLine, index, alias.Length)) + continue; + var column = index + 1; + if (!references.Any(reference => + reference.FileId == fileId + && reference.Line == lineNumber + && reference.Column == column + && reference.ReferenceKind == "type_reference" + && string.Equals(reference.SymbolName, alias, StringComparison.Ordinal))) + { + continue; + } + + var binding = FindActiveTypeAliasBinding(aliases, alias, lineNumber); + if (binding is null) + continue; + + TypedLanguageReferenceExtractor.EmitTypeExpressionReferences( + binding.Value.Target, + index, + "typescript", + references, + seen, + fileId, + context, + lineNumber, + resolveContainerForColumn(index), + binding.Value.TypeParameters); + } + } + } + + private static TypeAliasBinding? FindActiveTypeAliasBinding( + IReadOnlyList aliases, + string alias, + int lineNumber) + { + TypeAliasBinding? best = null; + foreach (var binding in aliases) + { + if (!string.Equals(binding.Alias, alias, StringComparison.Ordinal) + || lineNumber <= binding.BindingLine + || (binding.EndLine is int endLine && lineNumber > endLine) + || IsInsideScopedShadow(binding.ShadowRanges, lineNumber)) + { + continue; + } + + if (best is null + || binding.BraceDepth > best.Value.BraceDepth + || (binding.BraceDepth == best.Value.BraceDepth && binding.BindingLine > best.Value.BindingLine)) + { + best = binding; + } + } + + return best; + } + + private static IReadOnlyList BuildTypeAliasShadowRanges( + IReadOnlyList preparedLines, + IReadOnlyList braceDepths, + string alias) + { + var ranges = new List(); + for (var index = 0; index < preparedLines.Count; index++) + { + var line = preparedLines[index]; + var typeDeclaration = TypeDeclarationShadowRegex.Match(line); + if (typeDeclaration.Success && string.Equals(typeDeclaration.Groups["name"].Value, alias, StringComparison.Ordinal)) + { + ranges.Add(new LineRange(index + 1, FindScopedAliasEndLine(preparedLines, braceDepths, index) ?? preparedLines.Count)); + continue; + } + + if (DeclaresGenericTypeParameter(line, alias)) + ranges.Add(new LineRange(index + 1, FindScopedAliasEndLine(preparedLines, braceDepths, index) ?? index + 1)); + } + + return ranges; + } + + private static bool DeclaresGenericTypeParameter(string line, string alias) + { + var openAngle = line.IndexOf('<'); + if (openAngle < 0) + return false; + + var closeAngle = line.IndexOf('>', openAngle + 1); + if (closeAngle <= openAngle) + return false; + + var prefix = line[..openAngle]; + if (!ContainsKeyword(prefix, "class") + && !ContainsKeyword(prefix, "interface") + && !ContainsKeyword(prefix, "function") + && !ContainsKeyword(prefix, "type")) + { + return false; + } + + foreach (var parameter in line.Substring(openAngle + 1, closeAngle - openAngle - 1).Split(',')) + { + var name = parameter.Trim().Split([' ', '='], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + if (string.Equals(name, alias, StringComparison.Ordinal)) + return true; + } + + return false; + } + + private static IReadOnlySet ExtractGenericTypeParameters(string parameters) + { + var names = new HashSet(StringComparer.Ordinal); + var openAngle = parameters.IndexOf('<'); + var closeAngle = parameters.LastIndexOf('>'); + if (openAngle < 0 || closeAngle <= openAngle) + return names; + + foreach (var parameter in parameters.Substring(openAngle + 1, closeAngle - openAngle - 1).Split(',')) + { + var name = parameter.Trim().Split([' ', '=', ':'], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(name)) + names.Add(name); + } + + return names; + } + + private static bool ContainsKeyword(string text, string keyword) + { + var searchStart = 0; + while (searchStart < text.Length) + { + var index = text.IndexOf(keyword, searchStart, StringComparison.Ordinal); + if (index < 0) + return false; + + if (HasIdentifierBoundaries(text, index, keyword.Length)) + return true; + + searchStart = index + keyword.Length; + } + + return false; + } + + private static int? FindScopedAliasEndLine( + IReadOnlyList preparedLines, + IReadOnlyList braceDepths, + int bindingLineIndex) + { + var bindingDepth = braceDepths[bindingLineIndex]; + if (bindingDepth <= 0) + return null; + + for (var index = bindingLineIndex + 1; index < preparedLines.Count; index++) + { + if (braceDepths[index] < bindingDepth) + return index; + } + + return preparedLines.Count; + } + + private static string TrimAliasTarget(string target) + { + var equalsTarget = target.Trim(); + var stop = equalsTarget.Length; + foreach (var keyword in new[] { "extends", "implements" }) + { + var keywordIndex = FindTopLevelKeyword(equalsTarget, keyword); + if (keywordIndex >= 0) + stop = Math.Min(stop, keywordIndex); + } + + return equalsTarget[..stop].Trim(); + } + + private static int FindTopLevelKeyword(string line, string keyword) + { + foreach (var index in TypedLanguageReferenceExtractor.EnumerateTopLevelKeywordIndices(line, keyword)) + return index; + + return -1; + } + + private static bool HasIdentifierBoundaries(string line, int start, int length) + { + var before = start == 0 ? '\0' : line[start - 1]; + var afterIndex = start + length; + var after = afterIndex >= line.Length ? '\0' : line[afterIndex]; + return !IsTypeScriptIdentifierPart(before) && !IsTypeScriptIdentifierPart(after); + } + private static void EmitAsTypeReferences( string preparedLine, List references, diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs index 2880954945..e70549afd5 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs @@ -37,6 +37,12 @@ internal static List ExtractCore(ReferenceExtractionContext req var razorReferenceLines = preparedInput.RazorReferenceLines; var razorImplementedTypeNames = preparedInput.RazorImplementedTypeNames; var typeScriptNamespaceAliases = preparedInput.TypeScriptNamespaceAliases; + var typeScriptTypeAliases = language == "typescript" + ? TypeScriptReferenceExtractor.BuildTypeAliasTargets(preparedLines) + : null; + var swiftTypeAliases = language == "swift" + ? SwiftReferenceExtractor.BuildTypeAliasTargets(preparedLines) + : null; var jsTaggedTemplatesByLine = preparedInput.JsTaggedTemplatesByLine; // Pre-pass C# attribute analysis so cross-line `[\n Foo("x")\n]` and parameter // attributes `void M([Attr] T x)` are classified consistently with same-line `[Foo]`. @@ -795,6 +801,16 @@ bool ShouldSuppressDefinitionCall(string resolvedName, int callIndex) context, lineNumber, ResolveContainerForCall); + + TypeScriptReferenceExtractor.EmitAliasTargetReferences( + preparedLine, + typeScriptTypeAliases!, + references, + seen, + fileId, + context, + lineNumber, + ResolveContainerForCall); } else if (language == "kotlin") { @@ -818,6 +834,15 @@ bool ShouldSuppressDefinitionCall(string resolvedName, int callIndex) lineNumber, ResolveContainerForCall, ResolveSwiftPropertyContainerForCall); + SwiftReferenceExtractor.EmitAliasTargetReferences( + preparedLine, + swiftTypeAliases!, + references, + seen, + fileId, + context, + lineNumber, + ResolveContainerForCall); } else if (language == "rust") { diff --git a/src/CodeIndex/Indexer/References/Support/TypedLanguageReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Support/TypedLanguageReferenceExtractor.cs index 8c28e38f04..b76b97b01e 100644 --- a/src/CodeIndex/Indexer/References/Support/TypedLanguageReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Support/TypedLanguageReferenceExtractor.cs @@ -33,7 +33,8 @@ public static void EmitTypeExpressionReferences( fileId, context, lineNumber, - container)) + container, + ignoredSegments)) { return; } @@ -59,7 +60,8 @@ public static bool TryEmitTypeScriptFunctionTypeExpressionReferences( long fileId, string context, int lineNumber, - SymbolRecord? container) + SymbolRecord? container, + IReadOnlySet? ignoredSegments = null) { var leading = CountLeadingWhitespace(expression, 0, expression.Length); var trailing = CountTrailingWhitespace(expression, leading, expression.Length - leading); @@ -95,7 +97,8 @@ public static bool TryEmitTypeScriptFunctionTypeExpressionReferences( fileId, context, lineNumber, - container); + container, + ignoredSegments); var returnStart = SkipTypePrefixTrivia(normalizedExpression, arrowIndex + 2); if (returnStart >= normalizedExpression.Length) @@ -114,7 +117,8 @@ public static bool TryEmitTypeScriptFunctionTypeExpressionReferences( fileId, context, lineNumber, - container); + container, + ignoredSegments); return true; } @@ -129,7 +133,8 @@ private static void EmitTypeScriptFunctionParameterTypeReferences( long fileId, string context, int lineNumber, - SymbolRecord? container) + SymbolRecord? container, + IReadOnlySet? ignoredSegments) { if (paramStart < 0 || paramEnd <= paramStart || paramStart >= expression.Length) return; @@ -160,7 +165,8 @@ private static void EmitTypeScriptFunctionParameterTypeReferences( fileId, context, lineNumber, - container); + container, + ignoredSegments); } } diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 823d520ab3..f4d551d990 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -5754,6 +5754,175 @@ public void Extract_TypeScriptTypeAliasGenericDefaults_EmitsDefaultAndRhsTypeRef && reference.Context == "type Dict = Record;"); } + [Fact] + public void Extract_TypeScriptTypeAliasHeritage_EmitsUnderlyingTypeReference() + { + const string content = """ + class SomeType {} + type MyAlias = SomeType; + class Derived extends MyAlias {} + """; + + var symbols = SymbolExtractor.Extract(1, "typescript", content); + var references = ReferenceExtractor.Extract(1, "typescript", content, symbols); + + Assert.Contains(references, reference => + reference.SymbolName == "MyAlias" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "Derived"); + Assert.Contains(references, reference => + reference.SymbolName == "SomeType" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "Derived" + && reference.Context == "class Derived extends MyAlias {}"); + } + + [Fact] + public void Extract_TypeScriptTypeAliasMixedValueUse_OnlyExpandsTypePositionOccurrence() + { + const string content = """ + class SomeType {} + type MyAlias = SomeType; + function get(value: unknown) { return value; } + const x: MyAlias = get(MyAlias); + """; + + var symbols = SymbolExtractor.Extract(1, "typescript", content); + var references = ReferenceExtractor.Extract(1, "typescript", content, symbols); + + var expanded = references + .Where(reference => + reference.SymbolName == "SomeType" + && reference.ReferenceKind == "type_reference" + && reference.Context == "const x: MyAlias = get(MyAlias);") + .ToList(); + + Assert.Single(expanded); + Assert.Equal(10, expanded[0].Column); + } + + [Fact] + public void Extract_TypeScriptTypeAliasWithGenericDefault_EmitsUnderlyingTypeReference() + { + const string content = """ + class DefaultKey {} + class SomeType {} + class Arg {} + type MyAlias = SomeType & Box; + class Derived extends MyAlias {} + """; + + var symbols = SymbolExtractor.Extract(1, "typescript", content); + var references = ReferenceExtractor.Extract(1, "typescript", content, symbols); + + Assert.Contains(references, reference => + reference.SymbolName == "DefaultKey" + && reference.ReferenceKind == "type_reference" + && reference.Context == "type MyAlias = SomeType & Box;"); + Assert.Contains(references, reference => + reference.SymbolName == "SomeType" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "Derived" + && reference.Context == "class Derived extends MyAlias {}"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "T" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "Derived" + && reference.Context == "class Derived extends MyAlias {}"); + } + + [Fact] + public void Extract_TypeScriptFunctionTypeAlias_DoesNotEmitTypeParameterAsTarget() + { + const string content = """ + class SomeType {} + class Arg {} + type MyAlias = (value: T) => SomeType; + class Derived extends MyAlias {} + """; + + var symbols = SymbolExtractor.Extract(1, "typescript", content); + var references = ReferenceExtractor.Extract(1, "typescript", content, symbols); + + Assert.Contains(references, reference => + reference.SymbolName == "SomeType" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "Derived" + && reference.Context == "class Derived extends MyAlias {}"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "T" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "Derived" + && reference.Context == "class Derived extends MyAlias {}"); + } + + [Fact] + public void Extract_TypeScriptTypeAliasShadowedByScope_UsesActiveAliasBinding() + { + const string content = """ + class One {} + class Two {} + type MyAlias = One; + namespace Inner { + type MyAlias = Two; + export class B extends MyAlias {} + } + class A extends MyAlias {} + """; + + var symbols = SymbolExtractor.Extract(1, "typescript", content); + var references = ReferenceExtractor.Extract(1, "typescript", content, symbols); + + Assert.Contains(references, reference => + reference.SymbolName == "One" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "A" + && reference.Context == "class A extends MyAlias {}"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "Two" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "A" + && reference.Context == "class A extends MyAlias {}"); + Assert.Contains(references, reference => + reference.SymbolName == "Two" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "B" + && reference.Context == "export class B extends MyAlias {}"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "One" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "B" + && reference.Context == "export class B extends MyAlias {}"); + } + + [Fact] + public void Extract_TypeScriptTypeAliasShadowedByTypeDeclaration_DoesNotExpandOuterAlias() + { + const string content = """ + class One {} + type MyAlias = One; + namespace Inner { + class MyAlias {} + export class B extends MyAlias {} + } + class Box extends MyAlias {} + """; + + var symbols = SymbolExtractor.Extract(1, "typescript", content); + var references = ReferenceExtractor.Extract(1, "typescript", content, symbols); + + Assert.DoesNotContain(references, reference => + reference.SymbolName == "One" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "B" + && reference.Context == "export class B extends MyAlias {}"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "One" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "Box" + && reference.Context == "class Box extends MyAlias {}"); + } + [Fact] public void Extract_CsharpIndentedRawStringBeforeBlockComment_DoesNotLeakXmlDocReferences() { @@ -26211,6 +26380,146 @@ protocol Store { && reference.ReferenceKind == "type_reference"); } + [Fact] + public void Extract_SwiftTypealiasHeritage_EmitsUnderlyingTypeReference() + { + const string content = """ + class SomeType {} + typealias MyAlias = SomeType + class Derived: MyAlias {} + """; + + var symbols = SymbolExtractor.Extract(1, "swift", content); + var references = ReferenceExtractor.Extract(1, "swift", content, symbols); + + Assert.Contains(references, reference => + reference.SymbolName == "MyAlias" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "Derived"); + Assert.Contains(references, reference => + reference.SymbolName == "SomeType" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "Derived" + && reference.Context == "class Derived: MyAlias {}"); + } + + [Fact] + public void Extract_SwiftTypealiasMixedValueUse_OnlyExpandsTypePositionOccurrence() + { + const string content = """ + class SomeType {} + typealias MyAlias = SomeType + func get(_ value: Any) -> Any { value } + let x: MyAlias = get("MyAlias") + """; + + var symbols = SymbolExtractor.Extract(1, "swift", content); + var references = ReferenceExtractor.Extract(1, "swift", content, symbols); + + var expanded = references + .Where(reference => + reference.SymbolName == "SomeType" + && reference.ReferenceKind == "type_reference" + && reference.Context == "let x: MyAlias = get(\"MyAlias\")") + .ToList(); + + Assert.Single(expanded); + Assert.Equal(8, expanded[0].Column); + } + + [Fact] + public void Extract_SwiftGenericTypealiasHeritage_DoesNotEmitTypeParameterAsTarget() + { + const string content = """ + class SomeType {} + class Box {} + class Arg {} + typealias MyAlias = SomeType & Box + class Derived: MyAlias {} + """; + + var symbols = SymbolExtractor.Extract(1, "swift", content); + var references = ReferenceExtractor.Extract(1, "swift", content, symbols); + + Assert.Contains(references, reference => + reference.SymbolName == "SomeType" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "Derived" + && reference.Context == "class Derived: MyAlias {}"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "T" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "Derived" + && reference.Context == "class Derived: MyAlias {}"); + } + + [Fact] + public void Extract_SwiftTypealiasShadowedByScope_UsesActiveAliasBinding() + { + const string content = """ + class One {} + class Two {} + typealias MyAlias = One + enum Inner { + typealias MyAlias = Two + class B: MyAlias {} + } + class A: MyAlias {} + """; + + var symbols = SymbolExtractor.Extract(1, "swift", content); + var references = ReferenceExtractor.Extract(1, "swift", content, symbols); + + Assert.Contains(references, reference => + reference.SymbolName == "One" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "A" + && reference.Context == "class A: MyAlias {}"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "Two" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "A" + && reference.Context == "class A: MyAlias {}"); + Assert.Contains(references, reference => + reference.SymbolName == "Two" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "B" + && reference.Context == "class B: MyAlias {}"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "One" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "B" + && reference.Context == "class B: MyAlias {}"); + } + + [Fact] + public void Extract_SwiftTypealiasShadowedByTypeDeclaration_DoesNotExpandOuterAlias() + { + const string content = """ + class One {} + typealias MyAlias = One + enum Inner { + class MyAlias {} + class B: MyAlias {} + } + class Box: MyAlias {} + """; + + var symbols = SymbolExtractor.Extract(1, "swift", content); + var references = ReferenceExtractor.Extract(1, "swift", content, symbols); + + Assert.DoesNotContain(references, reference => + reference.SymbolName == "One" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "B" + && reference.Context == "class B: MyAlias {}"); + Assert.DoesNotContain(references, reference => + reference.SymbolName == "One" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "Box" + && reference.Context == "class Box: MyAlias {}"); + } + [Fact] public void Extract_SwiftExtensionTargets_RecordsExtendedTypes() {