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

Misc fixes for MSBuildEditor LSP #97

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
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
13 changes: 9 additions & 4 deletions Core.Tests/MonoDevelop.Xml.Core.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<!-- conditionally only build net48 on !windows so nunit doesn't try to run them -->
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('Windows'))">net48;net8.0</TargetFrameworks>
<TargetFrameworks>net48;net8.0</TargetFrameworks>
<NUnitDisableSupportAssemblies>true</NUnitDisableSupportAssemblies>
<RootNamespace>MonoDevelop.Xml.Tests</RootNamespace>
</PropertyGroup>
Expand All @@ -13,6 +11,12 @@
<Nullable Condition="$(TargetFramework)=='net48'">annotations</Nullable>
</PropertyGroup>

<!-- always build tests on net48, but only try to run tests on net48 when on windows -->
<PropertyGroup Condition="'$(TargetFramework)'=='net48' and !$([MSBuild]::IsOSPlatform('Windows'))">
<TestProject>False</TestProject>
<IsTestProject>False</IsTestProject>
</PropertyGroup>

<ItemGroup>
<None Remove="Resources\xhtml1-strict.xsd" />
<None Remove="Resources\XMLSchema.xsd" />
Expand All @@ -26,11 +30,12 @@
<ItemGroup>
<ProjectReference Include="..\Core\MonoDevelop.Xml.Core.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="NUnit3TestAdapter" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NUnit" />
<PackageReference Include="NUnit.Analyzers" />
<PackageReference Include="NUnit3TestAdapter" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<!-- upgrade vulnerable transitive dependencies -->
<PackageReference Include="System.Text.Json" />
Expand Down
15 changes: 12 additions & 3 deletions Core.Tests/Parser/ParsingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,13 @@ public void SpineParserRecoveryXhtmlStrictSchema ()
using var sr = new StreamReader (ResourceManager.GetXhtmlStrictSchema ());
var docTxt = sr.ReadToEnd ();

SpineParserRecovery (docTxt, 1127391);
}

[TestCase("<eee>foo\\</eee>", 25)]
[TestCase("<eee bar=\"1\" baz=\"hello\" />", 130)]
public void SpineParserRecovery (string docTxt, int expectedDelta)
{
var rootState = CreateRootState ();
var treeParser = new XmlTreeParser (rootState);
foreach (char c in docTxt) {
Expand All @@ -530,7 +537,7 @@ public void SpineParserRecoveryXhtmlStrictSchema ()
char c = docTxt[i];
spineParser.Push (c);

var recoveredParser = XmlSpineParser.FromDocumentPosition (rootState, doc, i).AssertNotNull ();
var recoveredParser = XmlSpineParser.FromDocumentPosition (rootState, doc, spineParser.Position).AssertNotNull ();
var delta = i - recoveredParser.Position;
totalNotRecovered += delta;

Expand All @@ -542,12 +549,14 @@ public void SpineParserRecoveryXhtmlStrictSchema ()
AssertEqual (spineParser.GetContext (), recoveredParser.GetContext ());
}

/*
int total = docTxt.Length * docTxt.Length / 2;
float recoveryRate = 1f - totalNotRecovered / (float)total;
TestContext.WriteLine ($"Recovered {(recoveryRate * 100f):F2}%");
*/

// check it never regresses
Assert.LessOrEqual (totalNotRecovered, 1118088);
Assert.LessOrEqual (totalNotRecovered, expectedDelta);
}

[TestCase ("<r>\r\n<a e='v' />\r\n</r>", "<r>\r\n<a e='v' /\r\n</r>")]
Expand All @@ -574,7 +583,7 @@ public void SpineParserRecoverFromError (string docTxt, string recoverFromDoc)
char c = docTxt[i];
spineParser.Push (c);

var recoveredParser = XmlSpineParser.FromDocumentPosition (rootState, doc, Math.Min (i, maxCompat)).AssertNotNull ();
var recoveredParser = XmlSpineParser.FromDocumentPosition (rootState, doc, Math.Min (i, maxCompat));

var end = Math.Min (i + 1, docTxt.Length);
for (int j = recoveredParser.Position; j < end; j++) {
Expand Down
2 changes: 1 addition & 1 deletion Core.Tests/Parser/TestXmlParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ public static void AssertDiagnosticCount (this IReadOnlyList<XmlDiagnostic>? dia
var sb = new System.Text.StringBuilder ();
sb.AppendLine ($"Expected {count} diagnostics, got {actualCount}:");
foreach (var err in filter is null? diagnostics : diagnostics.Where (filter)) {
sb.AppendLine ($"{err.Descriptor.Severity}@{err.Span}: {err.GetFormattedMessage ()}");
sb.AppendLine ($"{err.Descriptor.Severity}@{err.Span}: {err.GetFormattedMessageWithTitle ()}");
}
Assert.AreEqual (count, actualCount, sb.ToString ());
}
Expand Down
254 changes: 226 additions & 28 deletions Core.Tests/Utils/TextWithMarkers.cs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Core/Analysis/XmlDiagnostic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ public XmlDiagnostic (XmlDiagnosticDescriptor descriptor, TextSpan span, params
{
}

public string GetFormattedMessage () => Descriptor.GetFormattedMessage (messageArgs);
public string GetFormattedMessageWithTitle () => Descriptor.GetFormattedMessageWithTitle (messageArgs);
}
}
21 changes: 10 additions & 11 deletions Core/Analysis/XmlDiagnosticDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,34 @@ public class XmlDiagnosticDescriptor
public string Title { get; }

[StringSyntax (StringSyntaxAttribute.CompositeFormat)]
public string? Message { get; }
public string? MessageFormat { get; }
public XmlDiagnosticSeverity Severity { get; }

public XmlDiagnosticDescriptor (string id, string title, [StringSyntax (StringSyntaxAttribute.CompositeFormat)] string? message, XmlDiagnosticSeverity severity)
public XmlDiagnosticDescriptor (string id, string title, [StringSyntax (StringSyntaxAttribute.CompositeFormat)] string? messageFormat, XmlDiagnosticSeverity severity)
{
Title = title ?? throw new ArgumentNullException (nameof (title));
Id = id ?? throw new ArgumentNullException (nameof (id));
Message = message;
MessageFormat = messageFormat;
Severity = severity;
}

public XmlDiagnosticDescriptor (string id, string title, XmlDiagnosticSeverity severity)
: this (id, title, null, severity) { }

string? combinedMsg;

internal string GetFormattedMessage (object[]? args)
internal string GetFormattedMessageWithTitle (object[]? messageArgs)
{
try {
combinedMsg ??= (combinedMsg = Title + Environment.NewLine + Message);
if (args != null && args.Length > 0) {
return string.Format (combinedMsg, args);
}
string? message = messageArgs?.Length > 0 && MessageFormat is string format
? string.Format (MessageFormat, messageArgs)
: MessageFormat;
return string.IsNullOrEmpty (message)
? Title
: Title + Environment.NewLine + message;
} catch (FormatException ex) {
// this is likely to be called from somewhere other than where the diagnostic was constructed
// so ensure the error has enough info to track it down
throw new FormatException ($"Error formatting message for diagnostic {Id}", ex);
}
return combinedMsg;
}
}
}
4 changes: 2 additions & 2 deletions Core/Completion/XmlCompletionTriggering.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace MonoDevelop.Xml.Editor.Completion
{
class XmlCompletionTriggering
public class XmlCompletionTriggering
{
public static XmlCompletionTrigger GetTrigger (XmlSpineParser parser, XmlTriggerReason reason, char typedCharacter) => GetTriggerAndIncompleteSpan (parser, reason, typedCharacter).kind;

Expand Down Expand Up @@ -200,7 +200,7 @@ static bool TryGetReadForwardLength (ITextSource textSource, XmlSpineParser spin
};
}

enum XmlCompletionTrigger
public enum XmlCompletionTrigger
{
None,

Expand Down
7 changes: 7 additions & 0 deletions Core/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Runtime.CompilerServices;

// Declare this to get init properties. See https://github.com/dotnet/roslyn/issues/45510#issuecomment-694977239
internal static class IsExternalInit { }
9 changes: 9 additions & 0 deletions Core/Options/IOptionsReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace MonoDevelop.Xml.Options;

public interface IOptionsReader
{
bool TryGetOption<T> (Option<T> option, out T? value);
}
15 changes: 15 additions & 0 deletions Core/Options/OptionReaderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace MonoDevelop.Xml.Options;

public static class OptionReaderExtensions
{
public static T GetOption<T>(this IOptionsReader options, Option<T> option)
{
if (options.TryGetOption<T> (option, out T? value)) {
return value!;
}
return option.DefaultValue;
}
}
50 changes: 50 additions & 0 deletions Core/Options/Option`1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;

namespace MonoDevelop.Xml.Options;

/// <summary>
/// Defines an option that may affect formatter, editor, analyzer or code fix behavior.
/// Some of these are read from .editorconfig, and others may be mapped to equivalent settings
/// of the host IDE.
/// </summary>
public class Option<T>
{
public Option(string name, T defaultValue, bool isEditorConfigOption)
{
Name = name;
DefaultValue = defaultValue;
IsEditorConfigOption = isEditorConfigOption;
}

public Option(string name, T value, EditorConfigSerializer<T>? serializer = null) : this(name, value, true)
{
Serializer = serializer;
}

/// <summary>
/// A unique name for the option. If this is an editorconfig option, this will be used as the name
/// in .editorconfig.
/// </summary>
public string Name { get; }

/// <summary>
/// The value to use for this option when no setting is found in EditorConfig or
/// in the host.
/// </summary>
public T DefaultValue { get; }

/// <summary>
/// Whether this option will be read from .editorconfig.
/// </summary>
public bool IsEditorConfigOption { get; }

/// <summary>
/// Optionally override the EditorConfig serialization behavior
/// </summary>
public EditorConfigSerializer<T>? Serializer { get; }
}

public record EditorConfigSerializer<T> (Func<string, T> Deserialize, Func<T, string> Serialize);
32 changes: 32 additions & 0 deletions Core/Options/TextFormattingOptionValues.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;

namespace MonoDevelop.Xml.Options;

// based on https://github.com/dotnet/roslyn/blob/df4ae6b81013ac45367372176b9c3135a35a7e3c/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Formatting/LineFormattingOptions.cs
/// <summary>
/// Captures common text formatting options values from an <see cref="IOptionsReader"/>
/// so that they may be accessed more efficiently.
/// </summary>
public sealed record class TextFormattingOptionValues ()
{
public static readonly TextFormattingOptionValues Default = new ();

public bool ConvertTabsToSpaces { get; init; } = false;
public int TabSize { get; init; } = 4;
public int IndentSize { get; init; } = 4;
public string NewLine { get; init; } = Environment.NewLine;
public bool TrimTrailingWhitespace { get; init; } = false;

public TextFormattingOptionValues (IOptionsReader options)
: this ()
{
ConvertTabsToSpaces = options.GetOption (TextFormattingOptions.ConvertTabsToSpaces);
TabSize = options.GetOption (TextFormattingOptions.TabSize);
IndentSize = options.GetOption (TextFormattingOptions.IndentSize);
NewLine = options.GetOption (TextFormattingOptions.NewLine);
}
}
51 changes: 51 additions & 0 deletions Core/Options/TextFormattingOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;

namespace MonoDevelop.Xml.Options;

// based on https://github.com/dotnet/roslyn/blob/199c241cef61d94e25fcfd0f6bcaa91faa35d515/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Formatting/FormattingOptions2.cs#L23
/// <summary>
/// Options that control text formatting. Accessing these multiple times may be done more efficiently using <see cref="TextFormattingOptionValues"/>.
/// </summary>
public class TextFormattingOptions
{
public static readonly Option<bool> ConvertTabsToSpaces = new (
"indent_style",
TextFormattingOptionValues.Default.ConvertTabsToSpaces,
new EditorConfigSerializer<bool> (str => str != "tab", value => value ? "space" : "tab")
);

public static readonly Option<int> TabSize = new ("tab_size", TextFormattingOptionValues.Default.TabSize, true);

public static readonly Option<int> IndentSize = new ("indent_size", TextFormattingOptionValues.Default.IndentSize, true);

public static readonly Option<string> NewLine = new (
"end_of_line",
TextFormattingOptionValues.Default.NewLine,
new EditorConfigSerializer<string> (
str => str switch {
"lf" => "\n",
"cr" => "\r",
"crlf" => "\r\n",
_ => Environment.NewLine
},
value => value switch {
"\n" => "lf",
"\r" => "cr",
"\r\n" => "crlf",
_ => "unset"
}));


public static readonly Option<bool> InsertFinalNewline = new ("insert_final_newline", true, true);

public static readonly Option<bool> TrimTrailingWhitespace = new ("trim_trailing_whitespace", TextFormattingOptionValues.Default.TrimTrailingWhitespace, true);

public static readonly Option<int?> MaxLineLength = new ("max_line_length", null, new EditorConfigSerializer<int?> (
str => str != "off" && int.TryParse (str, out var val) && val > 0 ? val : null,
val => val.HasValue && val.Value > 0 ? val.Value.ToString () : "off"
));
}
32 changes: 32 additions & 0 deletions Core/Options/XmlFormattingOptions.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.

namespace MonoDevelop.Xml.Options;

/// <summary>
/// Options that control XML formatting
/// </summary>
public static class XmlFormattingOptions
{
public static readonly Option<bool> OmitXmlDeclaration = new ("xml_omit_declaration", false, true);
public static readonly Option<bool> IndentContent = new ("xml_indent_content", true, true);

public static readonly Option<bool> AttributesOnNewLine = new ("xml_attributes_on_new_line", false, true);
public static readonly Option<int> MaxAttributesPerLine = new ("xml_max_attributes_per_line", 10, true);

public static readonly Option<bool> AlignAttributes = new ("xml_align_attributes", false, true);
public static readonly Option<bool> AlignAttributeValues = new ("xml_align_attribute_values", false, true);
public static readonly Option<bool> WrapAttributes = new ("xml_wrap_attributes", false, true);
public static readonly Option<int> SpacesBeforeAssignment = new ("xml_spaces_before_assignment", 0, true);
public static readonly Option<int> SpacesAfterAssignment = new ("xml_spaces_after_assignment", 0, true);

public static readonly Option<char> QuoteChar = new ("xml_quote_style", '"', new EditorConfigSerializer<char> (
str => str == "single" ? '\'' : '"',
val => val == '\'' ? "single" : "double"
));

public static readonly Option<int> EmptyLinesBeforeStart = new ("xml_empty_lines_before_start", 0, true);
public static readonly Option<int> EmptyLinesAfterStart = new ("xml_empty_lines_after_start", 0, true);
public static readonly Option<int> EmptyLinesBeforeEnd = new ("xml_empty_lines_before_end", 0, true);
public static readonly Option<int> EmptyLinesAfterEnd = new ("xml_empty_lines_after_end", 0, true);
}
2 changes: 1 addition & 1 deletion Core/Parser/XmlParserContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ public override string ToString ()
builder.AppendLine ("Errors=");
foreach (XmlDiagnostic err in Diagnostics) {
builder.Append (' ', 4);
builder.AppendLine ($"[{err.Descriptor.Severity}@{err.Span}: {err.GetFormattedMessage ()}");
builder.AppendLine ($"[{err.Descriptor.Severity}@{err.Span}: {err.GetFormattedMessageWithTitle ()}");
}
}

Expand Down
Loading