Skip to content

Commit

Permalink
Merge pull request #147 from mhutch/warning-codes
Browse files Browse the repository at this point in the history
Add support for warning codes
  • Loading branch information
mhutch authored Feb 7, 2024
2 parents de854c4 + 23b20c3 commit 02cc781
Show file tree
Hide file tree
Showing 39 changed files with 2,180 additions and 368 deletions.
4 changes: 2 additions & 2 deletions MonoDevelop.MSBuild.Editor/Analysis/MSBuildSpellChecker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Task<SpellChecker> GetValueChecker (MSBuildDocument document, MSBuildValueKind k
lock (locker) {
if (customType is null) {
if (!valueKindCheckerTasks.TryGetValue (kind, out checker)) {
var knownVals = kind.GetSimpleValues (true);
var knownVals = kind.GetSimpleValues ();

valueKindCheckerTasks[kind] = checker = Task.Run (() =>
new SpellChecker (
Expand Down Expand Up @@ -167,7 +167,7 @@ public async Task<IEnumerable<ISymbol>> FindSimilarValues (MSBuildDocument docum

IEnumerable<ISymbol> GetValue (SpellChecker checker, MSBuildValueKind kind, CustomTypeInfo customType, string name)
{
var knownVals = (IReadOnlyList<ISymbol>)customType?.Values ?? kind.GetSimpleValues (true);
var knownVals = (IReadOnlyList<ISymbol>)customType?.Values ?? kind.GetSimpleValues ();
var valueComparer = (customType?.CaseSensitive ?? false) ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;

var knownValDict = knownVals.ToDictionary (v => v.Name, StringComparer.OrdinalIgnoreCase);
Expand Down
10 changes: 8 additions & 2 deletions MonoDevelop.MSBuild.Editor/Completion/MSBuildCompletionSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ async Task<List<CompletionItem>> GetExpressionCompletionsAsync (
{
var doc = context.Document;
var rr = context.ResolveResult;
var kind = valueSymbol.InferValueKindIfUnknown ();
var kind = valueSymbol.ValueKind;

if (!ValidateListPermitted (listKind, kind)) {
return null;
Expand All @@ -394,6 +394,12 @@ async Task<List<CompletionItem>> GetExpressionCompletionsAsync (
return null;
}

// FIXME: This is a temporary hack so we have completion for imported XSD schemas with missing type info.
// It is not needed for inferred schemas, as they have already performed the inference.
if (kind == MSBuildValueKind.Unknown) {
kind = MSBuildInferredSchema.InferValueKindFromName (valueSymbol);
}

bool isValue = triggerState == TriggerState.Value;

var items = new List<CompletionItem> ();
Expand Down Expand Up @@ -458,7 +464,7 @@ async Task<List<CompletionItem>> GetExpressionCompletionsAsync (
} else {
//FIXME: can we avoid awaiting this unless we actually need to resolve a function? need to propagate async downwards
await provider.FunctionTypeProvider.EnsureInitialized (token);
if (GetCompletionInfos (rr, triggerState, kind, triggerExpression, triggerLength, doc, provider.FunctionTypeProvider, fileSystem, Logger) is IEnumerable<ISymbol> completionInfos) {
if (GetCompletionInfos (rr, triggerState, valueSymbol, triggerExpression, triggerLength, doc, provider.FunctionTypeProvider, fileSystem, Logger, kindIfUnknown: kind) is IEnumerable<ISymbol> completionInfos) {
foreach (var ci in completionInfos) {
items.Add (CreateCompletionItem (context.DocumentationProvider, ci, XmlCompletionItemKind.AttributeValue));
}
Expand Down
7 changes: 7 additions & 0 deletions MonoDevelop.MSBuild.Editor/DisplayElementFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ public async Task<object> GetInfoTooltipElement (ITextBuffer buffer, MSBuildRoot

var elements = new List<object> { nameElement };

if (info is IInferredSymbol) {
elements.Add (
new ClassifiedTextElement (
new ClassifiedTextRun (PredefinedClassificationTypeNames.NaturalLanguage, "(inferred)", ClassifiedTextRunStyle.Italic)
));
}

switch (info.Description.DisplayElement) {
case IRoslynSymbol symbol:
await AddSymbolDescriptionElements (symbol, elements.Add, logger, token);
Expand Down
9 changes: 8 additions & 1 deletion MonoDevelop.MSBuild.Tests/Analyzers/CoreDiagnosticTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ public void InvalidBool ()
</PropertyGroup>
</Project>";

var schema = new MSBuildSchema {
new PropertyInfo ("EnableFoo", "", MSBuildValueKind.Bool)
};

var expected = new MSBuildDiagnostic (
CoreDiagnostics.InvalidBool,
SpanFromLineColLength (source, 3, 15, 9),
Expand All @@ -47,7 +51,10 @@ public void InvalidBool ()
.Add ("ValueKind", MSBuildValueKind.Bool)
);

VerifyDiagnostics (source, null, true, expected);
VerifyDiagnostics (source, out _,
includeCoreDiagnostics: true,
expectedDiagnostics: [expected],
schema: schema);
}


Expand Down
36 changes: 10 additions & 26 deletions MonoDevelop.MSBuild.Tests/Analyzers/MSBuildAnalyzerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,19 @@ class MSBuildAnalyzerTest
protected void VerifyDiagnostics (string source, MSBuildAnalyzer analyzer, params MSBuildDiagnostic[] expectedDiagnostics)
=> VerifyDiagnostics (source, out _, new[] { analyzer }, false, false, null, expectedDiagnostics);

protected void VerifyDiagnostics (string source, ICollection<MSBuildAnalyzer> analyzers, params MSBuildDiagnostic[] expectedDiagnostics)
=> VerifyDiagnostics (source, out _, analyzers, false, false, null, expectedDiagnostics);

protected void VerifyDiagnostics (string source, ICollection<MSBuildAnalyzer> analyzers, bool includeCoreDiagnostics, params MSBuildDiagnostic[] expectedDiagnostics)
=> VerifyDiagnostics (source, out _, analyzers, includeCoreDiagnostics, false, null, expectedDiagnostics);

protected void VerifyDiagnostics (string source, ICollection<MSBuildAnalyzer> analyzers, bool includeCoreDiagnostics, bool checkUnexpectedDiagnostics, params MSBuildDiagnostic[] expectedDiagnostics)
=> VerifyDiagnostics (source, out _, analyzers, includeCoreDiagnostics, checkUnexpectedDiagnostics, null, expectedDiagnostics);
protected void VerifyDiagnostics (
string source,
ICollection<MSBuildAnalyzer> analyzers = null,
bool includeCoreDiagnostics = false,
bool checkUnexpectedDiagnostics = false,
MSBuildSchema schema = null,
MSBuildDiagnostic[] expectedDiagnostics = null,
MSBuildRootDocument previousDocument = null
)
=> VerifyDiagnostics (source, out _, analyzers, includeCoreDiagnostics, checkUnexpectedDiagnostics, schema, expectedDiagnostics, previousDocument);

protected void VerifyDiagnostics (
string source,
Expand Down Expand Up @@ -146,26 +151,5 @@ protected TextSpan SpanFromLineColLength (string text, int line, int startCol, i
}
throw new ArgumentOutOfRangeException ($"Reached line {currentLine}");
}

}

class TestSchemaProvider : MSBuildSchemaProvider
{
readonly Dictionary<(string filename, string sdk), MSBuildSchema> schemas = new ();

public void AddTestSchema (string filename, string sdk, MSBuildSchema schema)
{
schemas.Add ((filename, sdk), schema);
}

public override MSBuildSchema GetSchema (string path, string sdk, out IList<MSBuildSchemaLoadError> loadErrors)
{
if (schemas.TryGetValue ((Path.GetFileName (path), sdk), out MSBuildSchema schema)) {
loadErrors = null;
return schema;
}

return base.GetSchema (path, sdk, out loadErrors);
}
}
}
32 changes: 32 additions & 0 deletions MonoDevelop.MSBuild.Tests/Analyzers/TestSchemaProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.IO;

using MonoDevelop.MSBuild.Schema;

namespace MonoDevelop.MSBuild.Tests.Analyzers
{
class TestSchemaProvider : MSBuildSchemaProvider
{
readonly Dictionary<(string filename, string sdk), MSBuildSchema> schemas = new ();

public void AddTestSchema (string filename, string sdk, MSBuildSchema schema)
{
schemas.Add ((filename, sdk), schema);
}

public override MSBuildSchema GetSchema (string path, string sdk, out IList<MSBuildSchemaLoadError> loadErrors)
{
if (schemas.TryGetValue ((Path.GetFileName (path), sdk), out MSBuildSchema schema)) {
loadErrors = null;
return schema;
}

return base.GetSchema (path, sdk, out loadErrors);
}

public override ICollection<MSBuildSchema> GetFallbackSchemas () => [];
}
}
47 changes: 47 additions & 0 deletions MonoDevelop.MSBuild.Tests/Completion/ExxpressionCompletionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Immutable;
using System.Linq;

using MonoDevelop.MSBuild.Analysis;
using MonoDevelop.MSBuild.Language;
using MonoDevelop.MSBuild.Language.Typesystem;
using MonoDevelop.MSBuild.Schema;

using NUnit.Framework;

namespace MonoDevelop.MSBuild.Tests.Completion;

[TestFixture]
class ExpressionCompletionTests : MSBuildExpressionCompletionTest
{
[Test]
public void WarningCodes ()
{
var source = @"<Project>
<PropertyGroup>
<NoWarn>|</NoWarn>
</PropertyGroup>
</Project>";


var schema = new MSBuildSchema () {
new CustomTypeInfo (
[
new CustomTypeValue ("CS123", null),
new CustomTypeValue ("CS456", null)
],
"csharp-warnings",
baseKind: MSBuildValueKind.WarningCode
),
new PropertyInfo ("NoWarn", "", MSBuildValueKind.WarningCode)
};

var completions = GetExpressionCompletion (source, out _, schema: schema).ToArray ();

Assert.AreEqual (2, completions.Length);
Assert.AreEqual ("CS123", completions[0].Name);
Assert.AreEqual ("CS456", completions[1].Name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

using MonoDevelop.MSBuild.Language;
using MonoDevelop.MSBuild.Language.Expressions;
using MonoDevelop.MSBuild.Language.Typesystem;
using MonoDevelop.MSBuild.Schema;
using MonoDevelop.MSBuild.Util;
using MonoDevelop.Xml.Dom;
using MonoDevelop.Xml.Parser;
using MonoDevelop.Xml.Tests;

using NUnit.Framework;

namespace MonoDevelop.MSBuild.Tests.Completion;

class MSBuildExpressionCompletionTest
{
[OneTimeSetUp]
public void LoadMSBuild () => MSBuildTestHelpers.RegisterMSBuildAssemblies ();

protected IEnumerable<ISymbol> GetExpressionCompletion (
string sourceWithMarkers,
out MSBuildRootDocument parsedDocument,
char triggerChar = '\0',
ExpressionCompletion.ExpressionTriggerReason reason = ExpressionCompletion.ExpressionTriggerReason.Invocation,
MSBuildSchema schema = null
)
{
const string projectFileName = "FakeProject.csproj";

var token = CancellationToken.None;

var schemas = new Analyzers.TestSchemaProvider ();
if (schema is not null) {
schemas.AddTestSchema (projectFileName, null, schema);
}

var environment = new NullMSBuildEnvironment ();
var taskMetadataBuilder = new NoopTaskMetadataBuilder ();

// internal errors should cause test failure
var logger = TestLoggerFactory.CreateTestMethodLogger ().RethrowExceptions ();

var textWithMarkers = TextWithMarkers.Parse (sourceWithMarkers, '|');
var source = textWithMarkers.Text;
int caretPos = textWithMarkers.GetMarkedPosition ();
var textSource = new StringTextSource (source);

parsedDocument = MSBuildRootDocument.Parse (
textSource,
projectFileName,
null,
schemas,
environment,
taskMetadataBuilder,
logger,
token);

var spineParser = XmlSpineParser.FromDocumentPosition (new XmlRootState (), parsedDocument.XDocument, caretPos);

var functionTypeProvider = new TestFunctionTypeProvider ();
var rr = MSBuildResolver.Resolve (spineParser.Clone (), textSource, parsedDocument, functionTypeProvider, logger);

Assert.NotNull (rr);

if (triggerChar == '\0' && reason == ExpressionCompletion.ExpressionTriggerReason.Invocation) {
reason = ExpressionCompletion.ExpressionTriggerReason.TypedChar;
}

// based on MSBuildCompletionSource.{GetExpressionCompletionsAsync,GetAdditionalCompletionsAsync}
// eventually we can factor out into a shared method

string expression = GetIncompleteValue (spineParser, textSource);
int exprStartPos = caretPos - expression.Length;
var triggerState = ExpressionCompletion.GetTriggerState (expression, caretPos - exprStartPos, reason, triggerChar, rr.IsCondition (),
out int spanStart, out int spanLength, out ExpressionNode triggerExpression, out var listKind, out IReadOnlyList<ExpressionNode> comparandVariables,
logger
);

if (triggerState == ExpressionCompletion.TriggerState.None) {
return [];
}

var valueSymbol = rr.GetElementOrAttributeValueInfo (parsedDocument);
if (valueSymbol is null || valueSymbol.ValueKind == MSBuildValueKind.Nothing) {
return [];
}

var kind = valueSymbol.ValueKind;
if (!ExpressionCompletion.ValidateListPermitted (listKind, kind)) {
return [];
}
kind = kind.WithoutModifiers ();
if (kind == MSBuildValueKind.Data || kind == MSBuildValueKind.Nothing) {
return null;
}

var fileSystem = new TestFilesystem ();

bool isValue = triggerState == ExpressionCompletion.TriggerState.Value;
if (comparandVariables != null && isValue) {
return ExpressionCompletion.GetComparandCompletions (parsedDocument, fileSystem, comparandVariables, logger);
}

return ExpressionCompletion.GetCompletionInfos (rr, triggerState, valueSymbol, triggerExpression, spanLength, parsedDocument, functionTypeProvider, fileSystem, logger);
}

// copied from XmlParserSnapshotExtensions, modified to use ITextSource instead of ITextSnapshot
static string GetIncompleteValue (XmlSpineParser spineAtCaret, ITextSource textSource)
{
int caretPosition = spineAtCaret.Position;
var node = spineAtCaret.Spine.Peek ();

int valueStart;
if (node is XText t) {
valueStart = t.Span.Start;
} else if (node is XElement el && el.IsEnded) {
valueStart = el.Span.End;
} else {
int lineStart = GetLineStart (textSource, caretPosition);
valueStart = spineAtCaret.Position - spineAtCaret.CurrentStateLength;
if (spineAtCaret.GetAttributeValueDelimiter ().HasValue) {
valueStart += 1;
}
valueStart = Math.Min (Math.Max (valueStart, lineStart), caretPosition);
}

return textSource.GetText (valueStart, caretPosition - valueStart);

static int GetLineStart (ITextSource textSource, int caretPosition)
{
if (caretPosition < 1) {
return caretPosition;
}
int lineStart = caretPosition - 1;
for (; lineStart >= 0; lineStart--) {
switch (textSource[caretPosition]) {
case '\r':
case '\n':
return lineStart + 1;
}
}
return lineStart;
}
}

class TestFunctionTypeProvider : IFunctionTypeProvider
{
public Task EnsureInitialized (CancellationToken token) => Task.CompletedTask;
public ClassInfo GetClassInfo (string name) => null;
public IEnumerable<ClassInfo> GetClassNameCompletions () => [];
public ISymbol GetEnumInfo (string reference) => null;
public FunctionInfo GetItemFunctionInfo (string name) => null;
public IEnumerable<FunctionInfo> GetItemFunctionNameCompletions () => [];
public FunctionInfo GetPropertyFunctionInfo (MSBuildValueKind valueKind, string name) => null;
public IEnumerable<FunctionInfo> GetPropertyFunctionNameCompletions (ExpressionNode triggerExpression) => [];
public FunctionInfo GetStaticPropertyFunctionInfo (string className, string name) => null;
public MSBuildValueKind ResolveType (ExpressionPropertyNode node) => MSBuildValueKind.Unknown;
}

class TestFilesystem : IMSBuildFileSystem
{
public bool DirectoryExists (string basePath) => false;

public IEnumerable<string> GetDirectories (string basePath) => [];

public IEnumerable<string> GetFiles (string basePath) => [];
}
}
Loading

0 comments on commit 02cc781

Please sign in to comment.