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

KnownReference: Add support for testing for referenced libraries #6726

Merged
merged 31 commits into from Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
695358b
Add KnowReference and tests
martin-strecker-sonarsource Feb 6, 2023
7547b03
Fix code smell (parameter names)
martin-strecker-sonarsource Feb 6, 2023
6b5d8f4
Add overloads for version passed as string.
martin-strecker-sonarsource Feb 7, 2023
5f39f96
Add support for public key checks
martin-strecker-sonarsource Feb 7, 2023
c739408
Move predicates to own file
martin-strecker-sonarsource Feb 7, 2023
d3842bd
Rename to IsReferencedBy
martin-strecker-sonarsource Feb 7, 2023
92deb06
Clean-up
martin-strecker-sonarsource Feb 7, 2023
8422676
Add argument null checks
martin-strecker-sonarsource Feb 7, 2023
ec96228
Clean-up
martin-strecker-sonarsource Feb 7, 2023
ab95c9b
Remove Or and replace with params
martin-strecker-sonarsource Feb 8, 2023
4caa689
argument naming
martin-strecker-sonarsource Feb 8, 2023
af2cdc8
test Naming
martin-strecker-sonarsource Feb 8, 2023
c4ae3a4
Move extension methods to the right file ine the right folder
martin-strecker-sonarsource Feb 9, 2023
657c32e
Add suing after move
martin-strecker-sonarsource Feb 9, 2023
9bef92a
Use IndexOf for "Contains"
martin-strecker-sonarsource Feb 10, 2023
9450d62
Adopt tests for string comparison
martin-strecker-sonarsource Feb 10, 2023
886b451
Use nested class
martin-strecker-sonarsource Feb 10, 2023
98364c7
Move to extensions folder
martin-strecker-sonarsource Feb 10, 2023
abed89f
Remove "Any" extension method
martin-strecker-sonarsource Feb 10, 2023
d619346
Rename parameter
martin-strecker-sonarsource Feb 10, 2023
6d341b1
Exception message
martin-strecker-sonarsource Feb 10, 2023
078a782
Rename KnownReference to KnownAssembly
martin-strecker-sonarsource Feb 13, 2023
f9bbb8a
Rename SpecialTypeMethod
martin-strecker-sonarsource Feb 13, 2023
0cd2ee6
Formatting
martin-strecker-sonarsource Feb 13, 2023
88e2f90
Move file
martin-strecker-sonarsource Feb 13, 2023
d2794d2
File scoped namespace
martin-strecker-sonarsource Feb 13, 2023
98ddc1b
Extract MockCompilation
martin-strecker-sonarsource Feb 13, 2023
0c21bd5
Simplify project setup for tests
martin-strecker-sonarsource Feb 13, 2023
54ed0dd
Rename method
martin-strecker-sonarsource Feb 13, 2023
5117f76
Remove EditorBrowsable
martin-strecker-sonarsource Feb 13, 2023
cb7633f
Add public key token to XUnit
martin-strecker-sonarsource Feb 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -31,5 +31,8 @@ public static class CompilationExtensions
// There's no direct way of checking compilation target framework yet (09/2020).
// See https://github.com/dotnet/roslyn/issues/3798
compilation.ObjectType.ContainingAssembly.Name == "mscorlib";

pavel-mikula-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
public static bool References(this Compilation compilation, KnownReference reference)
=> reference.IsReferencedBy(compilation);
}
}
@@ -0,0 +1,74 @@
/*
* SonarAnalyzer for .NET
* Copyright (C) 2015-2023 SonarSource SA
* mailto: contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

namespace SonarAnalyzer.Helpers;

public sealed partial class KnownReference
{
internal static Func<AssemblyIdentity, bool> NameIs(string name) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be internal? For testing purposes, add more static fields with some well-known libraries (like the xunit) that will use the feature.
For some hell-versions, take a look at TestMethodShouldContainAssertionTest for inspiration

Suggested change
internal static Func<AssemblyIdentity, bool> NameIs(string name) =>
private static Func<AssemblyIdentity, bool> NameIs(string name) =>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather like to test the single elements of the "DSL" for better test isolation. Relying on public static KnownReference XUnit_Assert { get; } and the like is problematic as these definitions change over time and any tests that rely on it will deteriorate. I did this to improve the situation

  • Encapsulate the methods in a nested internal Predicates class
  • Mark the class with [EditorBrowsable(EditorBrowsableState.Never)] to suppress IntelliSense from showing the nested class as much as possible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The EditorBrowsable is too strong solution. The nested class will do the trick.

x => x.Name.Equals(name);
pavel-mikula-sonarsource marked this conversation as resolved.
Show resolved Hide resolved

internal static Func<AssemblyIdentity, bool> StartsWith(string name) =>
x => x.Name.StartsWith(name);

internal static Func<AssemblyIdentity, bool> EndsWith(string name) =>
x => x.Name.EndsWith(name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a clear example in mind? Experience with KnownType shows that we typically exactly know what we're looking for.

Suggested change
internal static Func<AssemblyIdentity, bool> StartsWith(string name) =>
x => x.Name.StartsWith(name);
internal static Func<AssemblyIdentity, bool> EndsWith(string name) =>
x => x.Name.EndsWith(name);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For StartsWith I was thinking of something like Azure.Storage, Azure.ResourceManager, AWSSDK or Microsoft.Extensions and for Contains something like IdentityModel.Tokens or SqlClient or Logger

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will typically need to work with a price KnownType to do this check. And that one comes from a precisely known assembly => we should not need these.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are also checks like "IsTestProject" or "Is this targeting .Net Core" or "Is this a Web/WinForms/Console/WPF project" or "Is this a cloud app". These shortcuts can come in handy for such questions, especially for frameworks that have different implementations on different platforms like entity framework, SqlClient, or Asp.Net MVC.


internal static Func<AssemblyIdentity, bool> Contains(string name) =>
x => x.Name.Contains(name);

internal static Func<AssemblyIdentity, bool> VersionLowerThen(string version) =>
VersionLowerThen(Version.Parse(version));

internal static Func<AssemblyIdentity, bool> VersionLowerThen(Version version) =>
x => x.Version < version;

internal static Func<AssemblyIdentity, bool> VersionGreaterOrEqual(string version) =>
VersionGreaterOrEqual(Version.Parse(version));

internal static Func<AssemblyIdentity, bool> VersionGreaterOrEqual(Version version) =>
x => x.Version >= version;

internal static Func<AssemblyIdentity, bool> VersionBetween(string from, string to) =>
VersionBetween(Version.Parse(from), Version.Parse(to));

internal static Func<AssemblyIdentity, bool> VersionBetween(Version from, Version to) =>
x => x.Version >= from && x.Version <= to;

internal static Func<AssemblyIdentity, bool> OptionalPublicKeyTokenIs(string key) =>
x => !x.HasPublicKey || PublicKeyEqualHex(x, key);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for cases like StackExchange.Redis which comes with a strong name or without (others are e.g. Dapper or NitoAsyncEx).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we'll ever need to be that precise.

Suggested change
internal static Func<AssemblyIdentity, bool> OptionalPublicKeyTokenIs(string key) =>
x => !x.HasPublicKey || PublicKeyEqualHex(x, key);


internal static Func<AssemblyIdentity, bool> PublicKeyTokenIs(string key) =>
x => x.HasPublicKey && PublicKeyEqualHex(x, key);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Suggested change
internal static Func<AssemblyIdentity, bool> PublicKeyTokenIs(string key) =>
x => x.HasPublicKey && PublicKeyEqualHex(x, key);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is a strong name it is the most reliable source of identity. Also if we don't add the possibility to check for the strong name no one will ever add the functionality even in cases where it would be useful. The functionality is now there and fully covered by tests with our own public key.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to be that precise at all. We'll likely use these for an early bailout, as you plan to use them. And there will be no harm if we trigger on something that is a false match.

Let's remove it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also highly doubt that we would ever track a library that updates it's public key between versions.


private static bool PublicKeyEqualHex(AssemblyIdentity identity, string hexString)
{
var normalizedHexString = hexString.Replace("-", string.Empty);
return ArraysEqual(identity.PublicKeyToken.ToArray(), normalizedHexString) || ArraysEqual(identity.PublicKey.ToArray(), normalizedHexString);

static bool ArraysEqual(byte[] key, string hexString) =>
BitConverter.ToString(key).Replace("-", string.Empty).Equals(hexString, StringComparison.OrdinalIgnoreCase);
}

internal static Func<IEnumerable<AssemblyIdentity>, bool> Any(Func<AssemblyIdentity, bool> predicate) =>
pavel-mikula-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
predicate is null
? throw new ArgumentNullException(nameof(predicate))
: identities => identities.Any(predicate);
}
41 changes: 41 additions & 0 deletions analyzers/src/SonarAnalyzer.Common/Helpers/KnownReference.cs
@@ -0,0 +1,41 @@
/*
* SonarAnalyzer for .NET
* Copyright (C) 2015-2023 SonarSource SA
* mailto: contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

namespace SonarAnalyzer.Helpers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be file scoped namespace

{
public sealed partial class KnownReference
{
private readonly Func<IEnumerable<AssemblyIdentity>, bool> predicate;

internal KnownReference(Func<AssemblyIdentity, bool> predicate) : this(Any(predicate))
{
}
pavel-mikula-sonarsource marked this conversation as resolved.
Show resolved Hide resolved

internal KnownReference(Func<IEnumerable<AssemblyIdentity>, bool> predicate)
pavel-mikula-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
{
this.predicate = predicate ?? throw new ArgumentNullException(nameof(predicate));
}

public static KnownReference XUnit_Assert { get; } = new(NameIs("xunit.assert").Or(NameIs("xunit").And(VersionLowerThen("2.0"))));
pavel-mikula-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
pavel-mikula-sonarsource marked this conversation as resolved.
Show resolved Hide resolved

public bool IsReferencedBy(Compilation compilation) =>
predicate(compilation.ReferencedAssemblyNames);
}
}
@@ -0,0 +1,30 @@
/*
* SonarAnalyzer for .NET
* Copyright (C) 2015-2023 SonarSource SA
* mailto: contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

namespace SonarAnalyzer.Helpers;

internal static class KnownReferenceExtensions
pavel-mikula-sonarsource marked this conversation as resolved.
Show resolved Hide resolved
{
internal static Func<AssemblyIdentity, bool> And(this Func<AssemblyIdentity, bool> @this, Func<AssemblyIdentity, bool> predicate)
=> identity => @this(identity) && predicate(identity);

internal static Func<AssemblyIdentity, bool> Or(this Func<AssemblyIdentity, bool> @this, Func<AssemblyIdentity, bool> predicate)
=> identity => @this(identity) || predicate(identity);
}