public void BasicTests() { var parsed = Les2LanguageService.Value.Parse((UString) @" // Leading comment x = random.NextInt(); // Comment nodes extend a node's range, which could certainly, in principle, // affect LNodeRangeMapper's ability to do its job properly. But given // a range covering the text `x = random.NextInt()`, it should still decide // that the node for `x = random.NextInt()` is a better match than, say, `x` // or `random.NextInt()` or the `if` block below. if x > ExtraordinarilyLongIdentifier { return x - ExtraordinarilyLongIdentifier; } else { Log.Write(Severity.Error, ""Unexpectedly low x""); // Trailing comment return -(1); } // Trailing comment" , "Input.les").ToList(); // Replace first "return" statement with a synthetic call to #return LNode originalFirstReturn = parsed[1][1][0]; parsed[1] = parsed[1].WithArgChanged(1, parsed[1][1].WithArgChanged(0, LNode.Call(S.Return, LNode.List(originalFirstReturn[0])))); LNode assignment = parsed[0], @if = parsed[1], logWrite = @if[3][0], firstReturn = @if[1][0], random = assignment[1].Target[0]; // Verify we've correctly extracted parts of the input tree IsTrue(@if[1].Calls(S.Braces) && @if[3].Calls(S.Braces)); IsTrue(firstReturn.Calls(S.Return) && random.IsIdNamed("random")); AreEqual("Input.les", @if.Target.Range.Source.FileName); AreSame(EmptySourceFile.Synthetic, firstReturn.Range.Source); // Create a simulated output file mapping in which all indexes are multiplied by 2 IndexRange TweakedRange(IIndexRange r) => new IndexRange(r.StartIndex * 2, r.Length * 2); var mapper = new LNodeRangeMapper(); foreach (var node in parsed.SelectMany(stmt => stmt.DescendantsAndSelf()).Where(n => n.Range.StartIndex >= 0)) { mapper.SaveRange(node, TweakedRange(node.Range)); } // In real life, synthetic nodes do get their range saved, but here we must do it manually mapper.SaveRange(firstReturn, TweakedRange(originalFirstReturn.Range)); IndexRange CallFindRelatedNode_AndExpectFirstFoundNodeToBe(LNode node, IndexRange searchQuery, int maxSearchResults) { var list = mapper.FindRelatedNodes(searchQuery, 10); AreEqual(node, list[0].Item1); return(list[0].B); } // Let the tests begin. foreach (var node in new[] { assignment, @if, logWrite, random }) { // Given perfect input, FindRelatedNodes should always list the correct node first var range = CallFindRelatedNode_AndExpectFirstFoundNodeToBe(node, TweakedRange(node.Range), 10); AreEqual(TweakedRange(node.Range), range); // FindMostUsefulRelatedNode will find the same result var pair2 = mapper.FindMostUsefulRelatedNode(TweakedRange(node.Range), node.Range.Source); AreEqual(node, pair2.Item1); AreEqual(TweakedRange(node.Range), pair2.Item2); } // However, the firstReturn is synthetic. It can still be found with FindRelatedNodes(), // but `FindMostUsefulRelatedNode` won't return it because its source file is wrong. // Instead, the best match should be the first argument to #return. CallFindRelatedNode_AndExpectFirstFoundNodeToBe(firstReturn, TweakedRange(originalFirstReturn.Range), 10); var bestPair = mapper.FindMostUsefulRelatedNode(TweakedRange(originalFirstReturn.Range), originalFirstReturn.Range.Source); AreEqual(firstReturn[0], bestPair.Item1); AreEqual(TweakedRange(firstReturn[0].Range), bestPair.Item2); // Compute and test the target range for `x = random.NextInt()` with comments excluded var assignmentRange = new IndexRange(assignment[0].Range.StartIndex, assignment[1].Range.EndIndex); CallFindRelatedNode_AndExpectFirstFoundNodeToBe(assignment, TweakedRange(assignmentRange), 10); CallFindRelatedNode_AndExpectFirstFoundNodeToBe(assignment, TweakedRange(assignmentRange), 1); // Given a slightly skewed range, it should still find the nearest node. foreach (var node in new[] { assignment, @if, logWrite, random }) { CallFindRelatedNode_AndExpectFirstFoundNodeToBe(node, TweakedRange(node.Range).With(r => { r.StartIndex += 2; r.EndIndex += 2; }), 10); CallFindRelatedNode_AndExpectFirstFoundNodeToBe(node, TweakedRange(node.Range).With(r => { r.StartIndex -= 2; r.EndIndex -= 2; }), 10); CallFindRelatedNode_AndExpectFirstFoundNodeToBe(node, TweakedRange(node.Range).With(r => { r.StartIndex += 2; r.EndIndex -= 2; }), 10); CallFindRelatedNode_AndExpectFirstFoundNodeToBe(node, TweakedRange(node.Range).With(r => { r.StartIndex -= 2; r.EndIndex += 2; }), 10); // We don't need to ask for 10 search results either, not in code this simple CallFindRelatedNode_AndExpectFirstFoundNodeToBe(node, TweakedRange(node.Range).With(r => { r.StartIndex += 2; r.EndIndex += 2; }), 1); CallFindRelatedNode_AndExpectFirstFoundNodeToBe(node, TweakedRange(node.Range).With(r => { r.StartIndex -= 2; r.EndIndex -= 2; }), 1); CallFindRelatedNode_AndExpectFirstFoundNodeToBe(node, TweakedRange(node.Range).With(r => { r.StartIndex += 2; r.EndIndex -= 2; }), 1); CallFindRelatedNode_AndExpectFirstFoundNodeToBe(node, TweakedRange(node.Range).With(r => { r.StartIndex -= 2; r.EndIndex += 2; }), 1); } }
private static object RunCSharpCodeWithRoslyn(LNode parent, LNodeList code, IMacroContext context, ParsingMode printMode = null) { // Note: when using compileTimeAndRuntime, the transforms here affect the version // sent to Roslyn, but not the runtime version placed in the output file. code = code.SmartSelectMany(stmt => { // Ensure #r gets an absolute path; I don't know what Roslyn does with a // relative path (maybe WithMetadataResolver would let me control this, // but it's easier not to) if ((stmt.Calls(S.CsiReference, 1) || stmt.Calls(S.CsiLoad, 1)) && stmt[0].Value is string fileName) { fileName = fileName.Trim().WithoutPrefix("\"").WithoutSuffix("\""); var inputFolder = context.ScopedProperties.TryGetValue((Symbol)"#inputFolder", "").ToString(); var fullPath = Path.Combine(inputFolder, fileName); return(LNode.List(stmt.WithArgChanged(0, stmt[0].WithValue("\"" + fullPath + "\"")))); } // For each (top-level) LexicalMacro method, call #macro_context.RegisterMacro(). LNode attribute = null; if ((attribute = stmt.Attrs.FirstOrDefault( attr => AppearsToCall(attr, "LeMP", nameof(LexicalMacroAttribute).WithoutSuffix("Attribute")) || AppearsToCall(attr, "LeMP", nameof(LexicalMacroAttribute)))) != null && EcsValidators.MethodDefinitionKind(stmt, out _, out var macroName, out _, out _) == S.Fn) { var setters = SeparateAttributeSetters(ref attribute); attribute = attribute.WithTarget((Symbol)nameof(LexicalMacroAttribute)); setters.Insert(0, attribute); var newAttribute = F.Call(S.New, setters); var registrationCommand = F.Call(F.Dot(__macro_context, nameof(IMacroContext.RegisterMacro)), F.Call(S.New, F.Call(nameof(MacroInfo), F.Null, newAttribute, macroName))); return(LNode.List(stmt, registrationCommand)); } return(LNode.List(stmt)); }); var outputLocationMapper = new LNodeRangeMapper(); var options = new LNodePrinterOptions { IndentString = " ", SaveRange = outputLocationMapper.SaveRange }; string codeText = EcsLanguageService.WithPlainCSharpPrinter.Print(code, context.Sink, printMode, options); _roslynSessionLog?.WriteLine(codeText); _roslynSessionLog?.Flush(); _roslynScriptState.GetVariable(__macro_context_sanitized).Value = context; try { // Allow users to write messages via MessageSink.Default using (MessageSink.SetDefault(new MessageSinkFromDelegate((sev, ctx, msg, args) => { _roslynSessionLog?.Write("{0} from user ({1}): ", sev, MessageSink.GetLocationString(ctx)); _roslynSessionLog?.WriteLine(msg, args); context.Sink.Write(sev, ctx, msg, args); }))) { _roslynScriptState = _roslynScriptState.ContinueWithAsync(codeText).Result; } return(_roslynScriptState.ReturnValue); } catch (CompilationErrorException e) when(e.Diagnostics.Length > 0 && e.Diagnostics[0].Location.IsInSource) { // Determine the best location in the source code at which to report the error. // Keep in mind that the error may have occurred in a synthetic location not // found in the original code, and we cannot report such a location. Microsoft.CodeAnalysis.Text.TextSpan range = e.Diagnostics[0].Location.SourceSpan; var errorLocation = new IndexRange(range.Start, range.Length); var mappedErrorLocation = outputLocationMapper.FindRelatedNodes(errorLocation, 10) .FirstOrDefault(p => !p.A.Range.Source.Text.IsEmpty); string locationCaveat = ""; if (mappedErrorLocation.A != null) { bool mappedIsEarly = mappedErrorLocation.B.EndIndex <= errorLocation.StartIndex; if (mappedIsEarly || mappedErrorLocation.B.StartIndex >= errorLocation.EndIndex) { locationCaveat = "; " + "The error occurred at a location ({0}) that doesn't seem to exist in the original code.".Localized( mappedIsEarly ? "after the location indicated".Localized() : "before the location indicated".Localized()); } } // Extract the line where the error occurred, for inclusion in the error message int column = e.Diagnostics[0].Location.GetLineSpan().StartLinePosition.Character; int lineStart = range.Start - column; int lineEnd = codeText.IndexOf('\n', lineStart); if (lineEnd < lineStart) { lineEnd = codeText.Length; } string line = codeText.Substring(lineStart, lineEnd - lineStart); string errorMsg = e.Message + " - in «{0}»{1}".Localized(line, locationCaveat); context.Sink.Error(mappedErrorLocation.A ?? parent, errorMsg); LogRoslynError(e, context.Sink, parent, compiling: true); } catch (Exception e) { while (e is AggregateException ae && ae.InnerExceptions.Count == 1) { e = ae.InnerExceptions[0]; } context.Sink.Error(parent, "An exception was thrown from your code:".Localized() + " " + e.ExceptionMessageAndType()); LogRoslynError(e, context.Sink, parent, compiling: false); } return(NoValue.Value); }