diff --git a/SourceGeneratorToolkit.Test/MetadataFileTests.cs b/SourceGeneratorToolkit.Test/MetadataFileTests.cs
index e50caa6..4a2d5f4 100644
--- a/SourceGeneratorToolkit.Test/MetadataFileTests.cs
+++ b/SourceGeneratorToolkit.Test/MetadataFileTests.cs
@@ -111,6 +111,47 @@ public void ScopedFindLocationPicksTheOccurrenceAfterTheAnchor()
Assert.AreEqual(2, scoped.GetLineSpan().StartLinePosition.Line);
}
+ private const string CompoundNames = """
+ {"units":[
+ {"name":"SquareMeter","base":"Second"},
+ {"name":"Meter","base":"Second"}
+ ]}
+ """;
+
+ [TestMethod]
+ public void FindLocationSkipsALongerNameThatContainsTheNeedle()
+ {
+ MetadataFile file = new("units.json", CompoundNames, SourceText.From(CompoundNames), "/repo/units.json");
+
+ Location location = file.FindLocation("Meter");
+
+ Assert.AreEqual(2, location.GetLineSpan().StartLinePosition.Line);
+ Assert.AreEqual("Meter", CompoundNames.Substring(location.SourceSpan.Start, location.SourceSpan.Length));
+ Assert.AreEqual('"', CompoundNames[location.SourceSpan.Start - 1]);
+ }
+
+ [TestMethod]
+ public void ScopedFindLocationSkipsALongerNameThatContainsTheAnchor()
+ {
+ MetadataFile file = new("units.json", CompoundNames, SourceText.From(CompoundNames), "/repo/units.json");
+
+ Location location = file.FindLocation("Meter", "Second");
+
+ Assert.AreEqual(2, location.GetLineSpan().StartLinePosition.Line);
+ Assert.AreEqual("Second", CompoundNames.Substring(location.SourceSpan.Start, location.SourceSpan.Length));
+ }
+
+ [TestMethod]
+ public void FindLocationStillFindsTextThatIsNotAWholeString()
+ {
+ const string text = """{"things":[{"name":"Alpha","size":12345}]}""";
+ MetadataFile file = new("things.json", text, SourceText.From(text), "/repo/things.json");
+
+ Location location = file.FindLocation("2345");
+
+ Assert.AreEqual("2345", text.Substring(location.SourceSpan.Start, location.SourceSpan.Length));
+ }
+
[TestMethod]
public void ScopedFindLocationFallsBackWhenTheAnchorIsAbsent()
{
diff --git a/SourceGeneratorToolkit/MetadataFile.cs b/SourceGeneratorToolkit/MetadataFile.cs
index 02ccee2..b7330aa 100644
--- a/SourceGeneratorToolkit/MetadataFile.cs
+++ b/SourceGeneratorToolkit/MetadataFile.cs
@@ -86,7 +86,7 @@ public Location FindLocation(string needle)
return Location.None;
}
- int index = Text.IndexOf(needle, StringComparison.Ordinal);
+ int index = IndexOfToken(needle, 0);
return index < 0 ? Location.None : LocationAt(index, needle.Length);
}
@@ -113,16 +113,35 @@ public Location FindLocation(string anchor, string needle)
return Location.None;
}
- int scope = string.IsNullOrEmpty(anchor) ? 0 : Text.IndexOf(anchor, StringComparison.Ordinal);
+ int scope = string.IsNullOrEmpty(anchor) ? 0 : IndexOfToken(anchor, 0);
if (scope < 0)
{
return FindLocation(needle);
}
- int index = Text.IndexOf(needle, scope, StringComparison.Ordinal);
+ int index = IndexOfToken(needle, scope);
return index < 0 ? FindLocation(needle) : LocationAt(index, needle.Length);
}
+ ///
+ /// Finds at or after , preferring an
+ /// occurrence that is a whole JSON string.
+ ///
+ /// The text to find.
+ /// Where the search starts.
+ /// The index of the text itself, inside its quotes when it was found quoted, or -1.
+ ///
+ /// A plain substring search lands inside longer names: Meter is found in
+ /// "SquareMeter", which is a different entry. Looking for "Meter" first matches
+ /// only the whole string. The raw search remains the fallback so that text which is not a JSON
+ /// string on its own, such as a number or an already quoted anchor, is still found.
+ ///
+ private int IndexOfToken(string value, int startIndex)
+ {
+ int quoted = Text.IndexOf("\"" + value + "\"", startIndex, StringComparison.Ordinal);
+ return quoted >= 0 ? quoted + 1 : Text.IndexOf(value, startIndex, StringComparison.Ordinal);
+ }
+
///
/// Deserializes the file into .
///