Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Throw exception when circular reference is detected in LG imports #4779

Merged
merged 9 commits into from
Oct 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,21 @@ public static Templates ParseText(
return ParseResource(resource, importResolver, expressionParser);
}

/// <summary>
/// Parser to turn lg content into a <see cref="Templates"/>.
/// </summary>
/// <param name="resource">LG resource.</param>
/// <param name="importResolver">Resolver to resolve LG import id to template text.</param>
/// <param name="expressionParser">Expression parser for parsing expressions.</param>
/// <returns>new <see cref="Templates"/> entity.</returns>
public static Templates ParseResource(
LGResource resource,
ImportResolverDelegate importResolver = null,
ExpressionParser expressionParser = null)
{
return InnerParseResource(resource, importResolver, expressionParser);
}

/// <summary>
/// Parser to turn lg content into a <see cref="Templates"/> based on the original LGFile.
/// </summary>
Expand Down Expand Up @@ -148,19 +163,22 @@ public static IParseTree AntlrParseTemplates(LGResource resource)
/// <param name="importResolver">Resolver to resolve LG import id to template text.</param>
/// <param name="expressionParser">Expression parser for parsing expressions.</param>
/// <param name="cachedTemplates">Give the file path and templates to avoid parsing and to improve performance.</param>
/// <param name="parentTemplates">Parent visited Templates.</param>
/// <returns>new <see cref="Templates"/> entity.</returns>
public static Templates ParseResource(
private static Templates InnerParseResource(
LGResource resource,
ImportResolverDelegate importResolver = null,
ExpressionParser expressionParser = null,
Dictionary<string, Templates> cachedTemplates = null)
Dictionary<string, Templates> cachedTemplates = null,
Stack<Templates> parentTemplates = null)
Danieladu marked this conversation as resolved.
Show resolved Hide resolved
{
if (resource == null)
{
throw new ArgumentNullException(nameof(resource));
}

cachedTemplates = cachedTemplates ?? new Dictionary<string, Templates>();
parentTemplates = parentTemplates ?? new Stack<Templates>();
if (cachedTemplates.ContainsKey(resource.Id))
{
return cachedTemplates[resource.Id];
Expand All @@ -172,7 +190,7 @@ public static Templates ParseResource(
try
{
lg = new TemplatesTransformer(lg).Transform(AntlrParseTemplates(resource));
lg.References = GetReferences(lg, cachedTemplates);
lg.References = GetReferences(lg, cachedTemplates, parentTemplates);
new StaticChecker(lg).Check().ForEach(u => lg.Diagnostics.Add(u));
}
catch (TemplateException ex)
Expand Down Expand Up @@ -204,19 +222,19 @@ private static LGResource DefaultFileResolver(LGResource resource, string resour
return new LGResource(importPath, importPath, File.ReadAllText(importPath));
}

private static IList<Templates> GetReferences(Templates file, Dictionary<string, Templates> cachedTemplates = null)
private static IList<Templates> GetReferences(Templates file, Dictionary<string, Templates> cachedTemplates = null, Stack<Templates> parentTemplates = null)
{
var resourcesFound = new HashSet<Templates>();
ResolveImportResources(file, resourcesFound, cachedTemplates ?? new Dictionary<string, Templates>());
ResolveImportResources(file, resourcesFound, cachedTemplates ?? new Dictionary<string, Templates>(), parentTemplates ?? new Stack<Templates>());

resourcesFound.Remove(file);
return resourcesFound.ToList();
}

private static void ResolveImportResources(Templates start, HashSet<Templates> resourcesFound, Dictionary<string, Templates> cachedTemplates)
private static void ResolveImportResources(Templates start, HashSet<Templates> resourcesFound, Dictionary<string, Templates> cachedTemplates, Stack<Templates> parentTemplates)
{
resourcesFound.Add(start);

parentTemplates.Push(start);
foreach (var import in start.Imports)
{
LGResource resource;
Expand All @@ -231,6 +249,15 @@ private static void ResolveImportResources(Templates start, HashSet<Templates> r
throw new TemplateException(e.Message, new List<Diagnostic>() { diagnostic });
}

// Cycle reference would throw exception to avoid infinite Loop.
// Import self is allowed, and would ignore it.
if (parentTemplates.Peek().Id != resource.Id && parentTemplates.Any(u => u.Id == resource.Id))
{
var errorMsg = $"{TemplateErrors.LoopDetected} {resource.Id} => {start.Id}";
var diagnostic = new Diagnostic(import.SourceRange.Range, errorMsg, DiagnosticSeverity.Error, start.Source);
throw new TemplateException(errorMsg, new List<Diagnostic>() { diagnostic });
}

if (resourcesFound.All(u => u.Id != resource.Id))
{
Templates childResource;
Expand All @@ -240,13 +267,15 @@ private static void ResolveImportResources(Templates start, HashSet<Templates> r
}
else
{
childResource = ParseResource(resource, start.ImportResolver, start.ExpressionParser, cachedTemplates);
childResource = InnerParseResource(resource, start.ImportResolver, start.ExpressionParser, cachedTemplates, parentTemplates);
cachedTemplates.Add(resource.Id, childResource);
}

ResolveImportResources(childResource, resourcesFound, cachedTemplates);
ResolveImportResources(childResource, resourcesFound, cachedTemplates, parentTemplates);
}
}

parentTemplates.Pop();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
> Import CycleRef2.lg
[import](./ImportFile.lg)
[import](./CycleRef2.lg)
# a
- a
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
> Import CycleRef1.lg
[import](./CycleRef1.lg)
# b
- b
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
<None Remove="ExceptionExamples\RunTimeErrors.lg" />
<None Remove="ExceptionExamples\ExpressionFormatError.lg" />
<None Remove="ExceptionExamples\ErrorLine.lg" />
<None Remove="ExceptionExamples\CycleRef1.lg" />
<None Remove="ExceptionExamples\CycleRef2.lg" />
<None Remove="MultiLanguage\a.lg" />
<None Remove="MultiLanguage\a.en.lg" />
</ItemGroup>
Expand Down Expand Up @@ -334,6 +336,12 @@
<Content Include="ExceptionExamples\ErrorLine.lg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="ExceptionExamples\CycleRef1.lg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="ExceptionExamples\CycleRef2.lg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="MultiLanguage\a.lg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,14 @@ public void TestExpressionFormatError()
Assert.Contains("Close } is missing in Expression", diagnostics[0].Message);
}

[Fact]
public void TestLoopReference()
{
var diagnostics = GetDiagnostics("CycleRef1.lg");
Assert.Equal(1, diagnostics.Count);
Assert.StartsWith(TemplateErrors.LoopDetected, diagnostics[0].Message);
}

private string GetExceptionExampleFilePath(string fileName)
{
return AppContext.BaseDirectory.Substring(0, AppContext.BaseDirectory.IndexOf("bin")) + "ExceptionExamples" + Path.DirectorySeparatorChar + fileName;
Expand Down