예제 #1
0
        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);
            }
        }
예제 #2
0
        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);
        }