diff --git a/docs/Rules/MA0240.md b/docs/Rules/MA0240.md index 71a2e1353..52c757e74 100644 --- a/docs/Rules/MA0240.md +++ b/docs/Rules/MA0240.md @@ -334,7 +334,7 @@ The other attributes are the properties of the [symbol interfaces](https://learn - `@DeclaredAccessibility` has the values of `semantic:DeclaredAccessibility`, and it is not present for the symbols that have no accessibility, such as a local or a parameter. - `@ConstantValue` is the value of a constant field or of a constant local, and `@ExplicitDefaultValue` the default value of an optional parameter, formatted like `semantic:ConstantValue`. -- The kind, the locations, the containing assembly and the containing module of the symbol are not attributes, and the parameters and the type parameters are the child elements. +- The kind, the locations, the containing assembly and the containing module of the symbol are not attributes, and the parameters and the type parameters are the child elements. Use the [`containing-assembly`](#semantic-functions) function to test the assembly. | Query | Reported symbols | |-------|------------------| @@ -383,14 +383,129 @@ symbol(//ClassDeclaration[@Identifier='Sample'])/symbol:Method[@IsStatic='true'] | `//MethodDeclaration[symbol(.)/symbol:Parameter[@RefKind='Out']]` | The declarations of the methods that have an `out` parameter, reported as `MethodDeclaration` | | `symbol(//IdentifierName)[self::symbol:Local]` | The locals that are used, reported on their declaration | -- The function only returns the symbols that are in the document of the symbols, which are the symbols declared in the file. A node that refers to a symbol declared elsewhere, such as a call to `Console.WriteLine`, returns nothing: use the `semantic` attributes or the operations to select it. +- The function only returns the symbols that are in the document of the symbols, which are the symbols declared in the file. A node that refers to a symbol declared elsewhere, such as a call to `Console.WriteLine`, returns nothing: use the `semantic` attributes, the operations, or the [semantic functions](#semantic-functions) to select it. - A reference to a generic method or to a generic type returns its declaration, whatever its type arguments. - Several nodes can declare or refer to the same symbol, such as the declarations of a partial type, and the symbol is only returned once. - A query that uses the `symbol` function is evaluated on the syntax tree, with the semantic model, so it can use the `semantic` and the `symbol` prefixes, but not the `operation` prefix. The `syntax` function goes back to the same syntax tree, so its nodes have the `semantic` attributes too. +### Semantic functions + +The semantic functions test the type or the symbol of the node they are evaluated on, whatever the document the node belongs to. A query can then select a type by what it derives from, or a symbol by its attributes, by the members it overrides or implements, or by the assembly and the namespace that declare it, including the symbols declared in another assembly: + +````text +//symbol:NamedType[implements('System.IDisposable') and not(has-attribute('System.ObsoleteAttribute'))]; Seal the disposable types +//operation:Invocation[containing-assembly('Newtonsoft.Json')]; Use System.Text.Json +//InvocationExpression[has-attribute('T:System.ObsoleteAttribute')]; Do not call obsolete methods +//symbol:Method[overrides('M:System.Object.Equals(System.Object)')]; Implement IEquatable instead +//symbol:Local[is-captured()]; Do not capture locals in hot paths +//GotoStatement[not(contains(file-path(), '/Legacy/'))]; Use structured control flow instead +```` + +| Function | Returns | +|----------|---------| +| `implements(type[, format])` | `true` when the type implements the interface `type`, directly or through a base type or another interface. The type itself is not one of its interfaces | +| `inherits-from(type[, format])` | `true` when `type` is one of the base classes of the type. The type itself and its interfaces are not its base classes | +| `is-assignable-to(type[, format])` | `true` when the type is `type`, derives from it, or implements it | +| `has-attribute(type[, format])` | `true` when the symbol has an attribute whose class is `type` or derives from it | +| `attributes()` | The attributes applied to the symbol, as described below | +| `attributes(nodes)` | The attributes applied to the symbols of the nodes, so a query can select them outside of a predicate | +| `containing-assembly()` | The name of the assembly that declares the symbol, such as `System.Console`, or an empty string when there is none | +| `containing-assembly(name)` | `true` when the name of the assembly that declares the symbol is `name`. The case is ignored, as for the names of the assemblies | +| `is-from-current-assembly()` | `true` when the symbol is declared in the project that is analyzed | +| `containing-namespace()` | The namespace that contains the symbol, such as `System.Collections.Generic`, or an empty string for the global namespace | +| `containing-namespace(name)` | `true` when the namespace that contains the symbol is `name`. The namespaces `name` contains do not match, so use `starts-with(containing-namespace(), 'System.Data.')` to select them too | +| `overrides(member)` | `true` when the symbol overrides `member`, directly or through the members it overrides | +| `implements-member(member)` | `true` when the symbol implements `member`, a member of an interface of its containing type, implicitly or explicitly | +| `is-externally-visible()` | `true` when the symbol can be used from another assembly: the symbol and all its containing types are `public`, `protected`, or `protected internal`. A parameter or a type parameter is visible when the symbol that declares it is, and a local never is | +| `is-captured()` | `true` when the symbol is a local or a parameter that a lambda or a local function uses, which makes the compiler allocate a closure. The parameters of a primary constructor are not considered | +| `file-path()` | The path of the file that is analyzed, with `/` as the separator on every operating system. It does not depend on the node, and it does not need the semantic model | + +The functions are evaluated on the context node, so they are used in a predicate: `//symbol:NamedType[implements('System.IDisposable')]`. Only `attributes` also takes the nodes as an argument. + +`file-path` is the way to apply an entry to some files only, such as `//GotoStatement[not(contains(file-path(), '/Generated/'))]`, as the severity of an `.editorconfig` file applies to all the entries. + +#### The type and the symbol of a node + +The type functions use the type of the node, and the other ones use its symbol. A node that has no type or no symbol makes the functions return `false`, an empty string, or no node. When the context node is an attribute, such as `@Identifier`, the functions use the element that has the attribute. + +| Node | Type | Symbol | +|------|------|--------| +| A syntax node | The type of the expression, as `semantic:Type`, or else the type of the symbol | The symbol the node refers to or declares, as `semantic:Symbol` | +| An operation | The type of the operation, as `@Type` | The symbol the operation refers to: `@TargetMethod` for an invocation, `@Constructor` for an object creation, `@Member` for a reference to a field, a property, an event or a method, `@Local` and `@Parameter` for a reference to a local or to a parameter | +| A symbol | The type of the symbol | The symbol | +| An `AttributeData` element | The class of the attribute | The class of the attribute | +| An argument of an attribute | The type of the argument | None | + +The type of a symbol is the symbol itself when it is a type, and the type of a field, of a property, of an event, of a parameter, or of a local. A method has no type, as its return type is not the type of the method. The base classes and the interfaces of a type parameter are the ones of its constraints. + +The functions also work on the syntax nodes that the `syntax` function returns, so `syntax(//operation:Invocation)[has-attribute('System.ObsoleteAttribute')]` selects the calls to an obsolete method. A query that uses a semantic function other than `file-path` needs the semantic model, like an entry using the `semantic` attributes. + +#### The name of a type + +The `type` argument is in one of the formats of the attributes that expose a type. The `format` argument sets the format of the name. Without it, the format is detected from the name: + +| Format | Example | Detected when the name | Matches | +|--------|---------|------------------------|---------| +| `MetadataName` | ``System.Collections.Generic.IEnumerable`1``, `Outer+Nested` | has no other format | The type whatever its type arguments | +| `DocumentationDeclarationId` | ``T:System.Collections.Generic.IEnumerable`1`` | starts with a kind, such as `T:` | The type whatever its type arguments | +| `DocumentationReferenceId` | `System.Collections.Generic.IEnumerable{System.String}` | contains `{` | The type with these type arguments only | + +The names of the formats are case-sensitive. The variance is not taken into account, so a `List` is assignable to `IEnumerable{System.String}` but not to `IEnumerable{System.Object}`. + +| Query | Reported syntax | +|-------|-----------------| +| ``//symbol:Parameter[is-assignable-to('System.Collections.Generic.IEnumerable`1')]`` | The parameters of a type that implements `IEnumerable`, whatever `T` | +| `//symbol:Parameter[is-assignable-to('System.Collections.Generic.IEnumerable{System.String}')]` | The parameters of a type that implements `IEnumerable` | +| `//symbol:NamedType[inherits-from('System.Exception')]` | The exceptions | +| `//operation:ObjectCreation[implements('System.IDisposable')]` | The creations of a disposable object | +| `//symbol:Parameter[is-assignable-to('System.String', 'MetadataName')]` | The parameters of type `string` | +| `//InvocationExpression[not(is-from-current-assembly())]` | The calls to a method declared in another assembly | +| `//InvocationExpression[starts-with(containing-assembly(), 'Microsoft.')]` | The calls to a method declared in an assembly whose name starts with `Microsoft.` | + +#### The name of a member + +The `member` argument of `overrides` and `implements-member` is either the documentation comment id of the member, such as `M:System.Object.Equals(System.Object)`, which selects a single overload, or its name qualified by the metadata name of its containing type, such as `System.Object.Equals`, which selects all the overloads. These are the formats of `semantic:SymbolDocumentationId` and of `semantic:Symbol`. A qualified name cannot contain `:`, so the format is detected from the name. The member of a generic type is named after the definition of the type, such as ``System.IEquatable`1.Equals``. + +| Query | Reported syntax | +|-------|-----------------| +| `//symbol:Method[overrides('System.Object.Equals')]` | The overrides of `Object.Equals`, whatever the overload | +| `//symbol:Method[implements-member('M:System.IDisposable.Dispose')]` | The `Dispose` methods, including the explicit implementations | +| `//operation:Invocation[implements-member('M:System.IDisposable.Dispose')]` | The calls to a method that implements `IDisposable.Dispose`, such as `stream.Dispose()` | +| `//symbol:Property[overrides('P:Base.Value')]` | The overrides of the `Value` property of `Base` | +| `//symbol:Method[is-externally-visible()]` | The methods of the public API, which can be called from another assembly | +| `//InvocationExpression[containing-namespace('System.IO')]` | The calls to a method of a type of the `System.IO` namespace | + +#### Attributes + +The `attributes` function returns an `AttributeData` element for each attribute applied to the symbol. The children of an `AttributeData` element are its `ConstructorArgument` elements, then its `NamedArgument` elements. The items of an argument that is an array are its `Item` child elements. + +| Element | Attributes | +|---------|------------| +| `AttributeData` | `@AttributeClassName`, `@AttributeClassMetadataName`, `@AttributeClassDocumentationId`, `@AttributeClassReferenceId` and the other attributes of a type for the class of the attribute, and `@AttributeConstructor`, `@AttributeConstructorDocumentationId` and the other attributes of a symbol for its constructor | +| `ConstructorArgument` | `@Position`, the position of the argument, starting at `0` | +| `NamedArgument` | `@Name`, the name of the field or of the property the argument sets | +| `Item` | `@Position`, the position of the item in the array | + +The arguments and the items also have: + +- `@Kind`: the name of the member of [`TypedConstantKind`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.typedconstantkind): `Primitive`, `Enum`, `Type`, or `Array`. +- `@IsNull`: `true` when the value is `null`. +- `@Value`: the value formatted like `semantic:ConstantValue`. The value of a `typeof` is the metadata name of the type, and the value of an enumeration is its underlying value. An array has no value, as its items are its child elements. Like the other attributes, `@Value` is not present when the value is `null` or an empty string, so `[@IsNull='false' and not(@Value)]` selects the empty strings. +- `@TypeName`, `@TypeMetadataName` and the other attributes of a type, for the type of the argument. The type a `typeof` refers to has the attributes of a type too, prefixed by `Value`, such as `@ValueMetadataName`. + +An `AttributeData` element, and each of its arguments, is reported on the attribute in the file, and the diagnostic is `The syntax 'AttributeData' is banned`. The attributes applied in another file or in another assembly can be tested, but they are not reported. The `syntax` function returns the `Attribute` node of an `AttributeData` element or of one of its arguments. + +| Query | Reported syntax | +|-------|-----------------| +| `//symbol:Method[attributes()[@AttributeClassName='ObsoleteAttribute']/ConstructorArgument[@Value='TODO']]` | The obsolete methods whose message is `TODO` | +| `//symbol:NamedType[attributes()/NamedArgument[@Name='AllowMultiple' and @Value='true']]` | The attributes that can be applied several times | +| `//symbol:Method[attributes()/ConstructorArgument[@Kind='Type' and @Value='System.String']]` | The methods that have an attribute whose argument is `typeof(string)` | +| `attributes(//symbol:Method)[@AttributeClassMetadataName='System.ObsoleteAttribute']` | The `Obsolete` attributes applied to the methods, reported as `AttributeData` | +| `syntax(attributes(//symbol:Method))` | The attributes applied to the methods, reported as `Attribute` | + ### Invalid entries -The lines of the file that are not valid, such as a name that is not a member of `SyntaxKind`, an invalid XPath query, a query that does not return a node-set, a kind of operation or of symbol that does not exist, or an unknown severity, are reported by [MA0241](MA0241.md). The other lines of the file are still applied. +The lines of the file that are not valid, such as a name that is not a member of `SyntaxKind`, an invalid XPath query, a query that does not return a node-set, a kind of operation or of symbol that does not exist, a function called with the wrong number of arguments, an unknown format of a type name, or an unknown severity, are reported by [MA0241](MA0241.md). The other lines of the file are still applied. ## Example diff --git a/docs/Rules/MA0241.md b/docs/Rules/MA0241.md index 5ce173a2a..ddffdc2e3 100644 --- a/docs/Rules/MA0241.md +++ b/docs/Rules/MA0241.md @@ -10,7 +10,9 @@ The rule reports the lines of the `BannedSyntaxes.txt` and `BannedSyntaxes.*.txt A line is not valid when: - It is a single name, optionally preceded by `//`, that is not a member of `SyntaxKind`. The names are case-sensitive, so `gotostatement` is not valid. -- It is not a valid XPath 1.0 query, or it uses a function that does not exist. `syntax` and `symbol` are the only functions this rule adds to the ones of XPath 1.0. +- It is not a valid XPath 1.0 query, or it uses a function that does not exist. The functions this rule adds to the ones of XPath 1.0 are `syntax`, `symbol`, `implements`, `inherits-from`, `is-assignable-to`, `has-attribute`, `attributes`, `containing-assembly`, `is-from-current-assembly`, `containing-namespace`, `overrides`, `implements-member`, `is-externally-visible`, `is-captured`, and `file-path`. +- It calls one of these functions with the wrong number of arguments, such as `implements()`. +- The format given to `implements`, `inherits-from`, `is-assignable-to`, or `has-attribute` is a literal that is not `MetadataName`, `DocumentationDeclarationId`, or `DocumentationReferenceId`, such as `implements('System.IDisposable', 'Metadata')`. - The query does not return a node-set, such as `count(//ClassDeclaration)`. - It uses a namespace prefix other than `semantic`, `operation`, and `symbol`, which are the only ones that are defined. - It uses a name that is not one of the `semantic` attributes, such as `//ClassDeclaration[@semantic:Typo='a']`. diff --git a/src/Meziantou.Analyzer/Internals/AttributeDataElement.cs b/src/Meziantou.Analyzer/Internals/AttributeDataElement.cs new file mode 100644 index 000000000..28f36c873 --- /dev/null +++ b/src/Meziantou.Analyzer/Internals/AttributeDataElement.cs @@ -0,0 +1,38 @@ +namespace Meziantou.Analyzer.Internals; + +/// +/// An element of an : an attribute applied to a symbol, one of its arguments, or an +/// item of an argument that is an array. +/// +internal sealed class AttributeDataElement(string name, AttributeData attribute, TypedConstant constant, string? argumentName, int position) +{ + public const string AttributeDataName = "AttributeData"; + public const string ConstructorArgumentName = "ConstructorArgument"; + public const string NamedArgumentName = "NamedArgument"; + public const string ItemName = "Item"; + + /// The name of the element, such as AttributeData or ConstructorArgument. + public string Name { get; } = name; + + /// The attribute the element is, or the attribute that contains the argument the element is. + public AttributeData Attribute { get; } = attribute; + + /// The value of the argument or of the item. It is the default value for an attribute. + public TypedConstant Constant { get; } = constant; + + /// The name of a named argument. + public string? ArgumentName { get; } = argumentName; + + /// The position of a constructor argument or of an item. It is -1 for the other elements. + public int Position { get; } = position; + + public bool IsAttribute => string.Equals(Name, AttributeDataName, StringComparison.Ordinal); + + public AttributeDataElement? Parent { get; set; } + + public AttributeDataElement[] Children { get; set; } = []; + + public int IndexInParent { get; set; } + + internal XPathAttribute[]? Attributes { get; set; } +} diff --git a/src/Meziantou.Analyzer/Internals/AttributeDataForest.cs b/src/Meziantou.Analyzer/Internals/AttributeDataForest.cs new file mode 100644 index 000000000..176bd287e --- /dev/null +++ b/src/Meziantou.Analyzer/Internals/AttributeDataForest.cs @@ -0,0 +1,182 @@ +using System.Globalization; +using Microsoft.CodeAnalysis.Text; + +namespace Meziantou.Analyzer.Internals; + +/// +/// The attributes applied to a symbol, as the attributes function returns them. Each attribute is an element, +/// whose children are its constructor arguments followed by its named arguments. The items of an argument that is an +/// array are the children of the argument. It is built once per symbol and per file, and shared by the navigator and +/// all its clones. +/// +internal sealed class AttributeDataForest +{ + private static readonly TypeAttributeNames AttributeClassNames = XPathPropertyAttributes.CreateTypeNames("AttributeClass"); + private static readonly SymbolAttributeNames AttributeConstructorNames = XPathPropertyAttributes.CreateSymbolNames("AttributeConstructor"); + private static readonly TypeAttributeNames TypeNames = XPathPropertyAttributes.CreateTypeNames("Type"); + private static readonly TypeAttributeNames ValueTypeNames = XPathPropertyAttributes.CreateTypeNames("Value"); + + private readonly SyntaxTree? _syntaxTree; + private readonly CancellationToken _cancellationToken; + + private AttributeDataForest(AttributeDataElement[] roots, SyntaxTree? syntaxTree, CancellationToken cancellationToken) + { + Roots = roots; + _syntaxTree = syntaxTree; + _cancellationToken = cancellationToken; + } + + /// The attributes applied to the symbol, in the order Roslyn returns them. + public AttributeDataElement[] Roots { get; } + + public CancellationToken CancellationToken => _cancellationToken; + + /// The symbol the attributes are applied to. + /// The file that is analyzed. Only the attributes applied in this file are reportable. + /// The cancellation token of the analysis. + public static AttributeDataForest Create(ISymbol symbol, SyntaxTree? syntaxTree, CancellationToken cancellationToken) + { + var attributes = symbol.GetAttributes(); + var roots = new AttributeDataElement[attributes.Length]; + for (var i = 0; i < attributes.Length; i++) + { + var attribute = attributes[i]; + var element = new AttributeDataElement(AttributeDataElement.AttributeDataName, attribute, constant: default, argumentName: null, position: -1) { IndexInParent = i }; + var children = new List(attribute.ConstructorArguments.Length + attribute.NamedArguments.Length); + for (var position = 0; position < attribute.ConstructorArguments.Length; position++) + { + children.Add(CreateArgument(AttributeDataElement.ConstructorArgumentName, attribute, attribute.ConstructorArguments[position], argumentName: null, position)); + } + + foreach (var namedArgument in attribute.NamedArguments) + { + children.Add(CreateArgument(AttributeDataElement.NamedArgumentName, attribute, namedArgument.Value, namedArgument.Key, position: -1)); + } + + SetChildren(element, children); + roots[i] = element; + } + + return new AttributeDataForest(roots, syntaxTree, cancellationToken); + } + + private static AttributeDataElement CreateArgument(string name, AttributeData attribute, TypedConstant constant, string? argumentName, int position) + { + var element = new AttributeDataElement(name, attribute, constant, argumentName, position); + if (constant is { Kind: TypedConstantKind.Array, IsNull: false }) + { + var items = new List(constant.Values.Length); + for (var i = 0; i < constant.Values.Length; i++) + { + items.Add(CreateArgument(AttributeDataElement.ItemName, attribute, constant.Values[i], argumentName: null, i)); + } + + SetChildren(element, items); + } + + return element; + } + + private static void SetChildren(AttributeDataElement parent, List children) + { + for (var i = 0; i < children.Count; i++) + { + children[i].Parent = parent; + children[i].IndexInParent = i; + } + + parent.Children = [.. children]; + } + + /// + /// The span of the attribute an element belongs to, when the attribute is applied in the file that is analyzed. + /// An argument is reported on its attribute, as a params array has no syntax of its own. + /// + public ImmutableArray GetSpans(AttributeDataElement element) + { + if (element.Attribute.ApplicationSyntaxReference is { } reference && reference.SyntaxTree == _syntaxTree) + return ImmutableArray.Create(reference.Span); + + return ImmutableArray.Empty; + } + + /// + /// The syntax of the attribute an element belongs to, when the attribute is applied in the file that is analyzed. + /// + public SyntaxNode? GetSyntaxNode(AttributeDataElement element) + { + if (element.Attribute.ApplicationSyntaxReference is { } reference && reference.SyntaxTree == _syntaxTree) + return reference.GetSyntax(_cancellationToken); + + return null; + } + + /// + /// The value of an element: the attribute as Roslyn displays it, or the value of an argument. + /// + public static string GetValue(AttributeDataElement element) => element.IsAttribute ? element.Attribute.ToString() ?? "" : FormatValue(element.Constant) ?? ""; + + /// + /// The attributes of an element. The attributes of the elements are few, so all of them are computed, once. + /// + public XPathAttribute[] GetAttributes(AttributeDataElement element) + { + return element.Attributes ??= BuildAttributes(element, GetSpans(element)); + } + + private static XPathAttribute[] BuildAttributes(AttributeDataElement element, ImmutableArray spans) + { + var attributes = new List(); + var writer = new XPathAttributeWriter(attributes, namespaceUri: "", qualifiedNames: null, XPathAttributeFilter.All); + var span = spans.IsDefaultOrEmpty ? default : spans[0]; + if (element.IsAttribute) + { + XPathAttributeFormatter.AddType(writer, span, element.Attribute.AttributeClass, AttributeClassNames); + XPathAttributeFormatter.AddSymbol(writer, span, element.Attribute.AttributeConstructor, AttributeConstructorNames); + } + else + { + var constant = element.Constant; + writer.Add(span, AttributeName.Name, element.ArgumentName); + if (element.Position >= 0) + { + writer.Add(span, AttributeName.Position, element.Position.ToString(CultureInfo.InvariantCulture)); + } + + writer.Add(span, AttributeName.Kind, constant.Kind.ToString()); + writer.Add(span, AttributeName.IsNull, XPathAttributeFormatter.ToXPathBoolean(constant.IsNull)); + writer.Add(span, AttributeName.Value, FormatValue(constant)); + XPathAttributeFormatter.AddType(writer, span, constant.Type, TypeNames); + if (constant is { Kind: TypedConstantKind.Type, IsNull: false }) + { + XPathAttributeFormatter.AddType(writer, span, constant.Value as ITypeSymbol, ValueTypeNames); + } + } + + return attributes.Count is 0 ? [] : [.. attributes]; + } + + // The value of an array is the items, which are the child elements. The value of a type is its metadata name, and + // the value of an enumeration is its underlying value. + private static string? FormatValue(TypedConstant constant) + { + if (constant.IsNull) + return null; + + return constant.Kind switch + { + TypedConstantKind.Array or TypedConstantKind.Error => null, + TypedConstantKind.Type => SymbolNameFormatter.GetMetadataName(constant.Value as ITypeSymbol), + _ => XPathAttributeFormatter.FormatConstantValue(constant.Value), + }; + } + + private static class AttributeName + { + public const string Name = nameof(Name); + public const string Position = nameof(Position); + public const string Kind = nameof(Kind); + public const string IsNull = nameof(IsNull); + public const string Value = nameof(Value); + } +} diff --git a/src/Meziantou.Analyzer/Internals/AttributeDataXPathNavigator.cs b/src/Meziantou.Analyzer/Internals/AttributeDataXPathNavigator.cs new file mode 100644 index 000000000..531133062 --- /dev/null +++ b/src/Meziantou.Analyzer/Internals/AttributeDataXPathNavigator.cs @@ -0,0 +1,248 @@ +using System.Xml; +using System.Xml.XPath; +using Microsoft.CodeAnalysis.Text; + +namespace Meziantou.Analyzer.Internals; + +/// +/// Exposes the attributes applied to a symbol as an XML document, so the attributes function can return them. +/// Each attribute is an AttributeData element, whose children are its ConstructorArgument and +/// NamedArgument elements. The items of an argument that is an array are its Item child elements. +/// +internal sealed class AttributeDataXPathNavigator : XPathNavigator, IBannedSyntaxNavigator +{ + private readonly AttributeDataForest _forest; + private readonly XmlNameTable _nameTable; + + // null when the navigator is positioned on the document, which is the parent of the roots + private AttributeDataElement? _element; + private XPathAttribute[]? _attributes; + private int _attributeIndex = -1; + + public AttributeDataXPathNavigator(AttributeDataForest forest) + { + _forest = forest; + _nameTable = new NameTable(); + } + + private AttributeDataXPathNavigator(AttributeDataXPathNavigator other) + { + _forest = other._forest; + _nameTable = other._nameTable; + _element = other._element; + _attributes = other._attributes; + _attributeIndex = other._attributeIndex; + } + + /// + /// The forest the navigator moves in. + /// + public AttributeDataForest Forest => _forest; + + /// + /// The element the navigator is positioned on, or the element that owns the attribute it is positioned on. + /// + public AttributeDataElement? Element => _element; + + /// + /// The name of the attribute the navigator is positioned on, if any. + /// + public string? AttributeName => IsOnAttribute ? _attributes![_attributeIndex].Name : null; + + /// + /// The span of the attribute the element belongs to, when it is applied in the file that is analyzed. + /// + public ImmutableArray ReportSpans => _element is null ? ImmutableArray.Empty : _forest.GetSpans(_element); + + /// + /// The name of the element, followed by the name of the attribute when the navigator is positioned on one, such + /// as AttributeData/@AttributeClassName. + /// + public string? ReportName + { + get + { + if (_element is null) + return null; + + return AttributeName is { } attributeName ? _element.Name + "/@" + attributeName : _element.Name; + } + } + + private bool IsOnAttribute => _attributeIndex >= 0; + + public override XmlNameTable NameTable => _nameTable; + + public override XPathNodeType NodeType + { + get + { + if (IsOnAttribute) + return XPathNodeType.Attribute; + + return _element is null ? XPathNodeType.Root : XPathNodeType.Element; + } + } + + public override string LocalName + { + get + { + if (IsOnAttribute) + return _attributes![_attributeIndex].LocalName; + + return _element is null ? "" : _element.Name; + } + } + + public override string Name => LocalName; + + // The elements and the attributes are not in a namespace + public override string NamespaceURI => ""; + + public override string Prefix => ""; + + public override string BaseURI => ""; + + public override bool IsEmptyElement => _element is not null && !IsOnAttribute && _element.Children.Length is 0; + + public override string Value + { + get + { + if (IsOnAttribute) + return _attributes![_attributeIndex].Value; + + return _element is null ? "" : AttributeDataForest.GetValue(_element); + } + } + + public override XPathNavigator Clone() => new AttributeDataXPathNavigator(this); + + /// + /// A navigator on the same forest, positioned on an element of it. It is how the attributes function + /// returns the attributes of a symbol. + /// + public AttributeDataXPathNavigator CreateAt(AttributeDataElement element) + { + var navigator = new AttributeDataXPathNavigator(this); + navigator.SetElement(element); + return navigator; + } + + public override bool IsSamePosition(XPathNavigator other) + { + return other is AttributeDataXPathNavigator navigator + && ReferenceEquals(navigator._forest, _forest) + && ReferenceEquals(navigator._element, _element) + && navigator._attributeIndex == _attributeIndex; + } + + public override bool MoveTo(XPathNavigator other) + { + if (other is not AttributeDataXPathNavigator navigator || !ReferenceEquals(navigator._forest, _forest)) + return false; + + _element = navigator._element; + _attributes = navigator._attributes; + _attributeIndex = navigator._attributeIndex; + return true; + } + + public override bool MoveToFirstAttribute() + { + if (_element is null || IsOnAttribute) + return false; + + _attributes ??= _forest.GetAttributes(_element); + if (_attributes.Length is 0) + return false; + + _attributeIndex = 0; + return true; + } + + public override bool MoveToNextAttribute() + { + if (!IsOnAttribute || _attributeIndex + 1 >= _attributes!.Length) + return false; + + _attributeIndex++; + return true; + } + + public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) => false; + + public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope) => false; + + public override bool MoveToId(string id) => false; + + public override bool MoveToFirstChild() + { + _forest.CancellationToken.ThrowIfCancellationRequested(); + if (IsOnAttribute) + return false; + + var children = _element is null ? _forest.Roots : _element.Children; + if (children.Length is 0) + return false; + + SetElement(children[0]); + return true; + } + + public override bool MoveToNext() => MoveToSibling(step: 1); + + public override bool MoveToPrevious() => MoveToSibling(step: -1); + + public override bool MoveToParent() + { + if (IsOnAttribute) + { + _attributeIndex = -1; + return true; + } + + if (_element is null) + return false; + + if (_element.Parent is not { } parent) + { + MoveToRoot(); + return true; + } + + SetElement(parent); + return true; + } + + public override void MoveToRoot() + { + _element = null; + _attributes = null; + _attributeIndex = -1; + } + + private bool MoveToSibling(int step) + { + _forest.CancellationToken.ThrowIfCancellationRequested(); + if (IsOnAttribute || _element is null) + return false; + + // The attributes are the children of the document, so they are siblings of each other + var siblings = _element.Parent is { } parent ? parent.Children : _forest.Roots; + var index = _element.IndexInParent + step; + if (index < 0 || index >= siblings.Length) + return false; + + SetElement(siblings[index]); + return true; + } + + private void SetElement(AttributeDataElement element) + { + _element = element; + _attributes = null; + _attributeIndex = -1; + } +} diff --git a/src/Meziantou.Analyzer/Internals/BannedSyntaxXsltContext.cs b/src/Meziantou.Analyzer/Internals/BannedSyntaxXsltContext.cs index 3d3a1ab88..111bc37e4 100644 --- a/src/Meziantou.Analyzer/Internals/BannedSyntaxXsltContext.cs +++ b/src/Meziantou.Analyzer/Internals/BannedSyntaxXsltContext.cs @@ -1,13 +1,17 @@ +using System.Globalization; using System.Xml; using System.Xml.XPath; using System.Xml.Xsl; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Meziantou.Analyzer.Internals; /// /// The context of the XPath queries of the banned syntax files. It defines the semantic, operation and /// symbol prefixes, the syntax function, which returns the syntax nodes of the operations or of the -/// symbols it is given, and the symbol function, which returns the symbols of the syntax nodes it is given. +/// symbols it is given, the symbol function, which returns the symbols of the syntax nodes it is given, and the +/// semantic functions, which test the type or the symbol of the node they are evaluated on, such as implements +/// or has-attribute. /// internal sealed class BannedSyntaxXsltContext : XsltContext { @@ -17,15 +21,73 @@ internal sealed class BannedSyntaxXsltContext : XsltContext /// The name of the function that returns the symbols of a set of syntax nodes. public const string SymbolFunctionName = "symbol"; + /// The name of the function that indicates whether the type of a node implements an interface. + public const string ImplementsFunctionName = "implements"; + + /// The name of the function that indicates whether the type of a node derives from a class. + public const string InheritsFromFunctionName = "inherits-from"; + + /// The name of the function that indicates whether the type of a node is, derives from, or implements a type. + public const string IsAssignableToFunctionName = "is-assignable-to"; + + /// The name of the function that indicates whether the symbol of a node has an attribute. + public const string HasAttributeFunctionName = "has-attribute"; + + /// The name of the function that returns the attributes applied to the symbol of a node. + public const string AttributesFunctionName = "attributes"; + + /// The name of the function that returns or tests the name of the assembly that contains the symbol of a node. + public const string ContainingAssemblyFunctionName = "containing-assembly"; + + /// The name of the function that indicates whether the symbol of a node is declared in the compilation. + public const string IsFromCurrentAssemblyFunctionName = "is-from-current-assembly"; + + /// The name of the function that returns or tests the namespace that contains the symbol of a node. + public const string ContainingNamespaceFunctionName = "containing-namespace"; + + /// The name of the function that indicates whether the symbol of a node overrides a member. + public const string OverridesFunctionName = "overrides"; + + /// The name of the function that indicates whether the symbol of a node implements a member of an interface. + public const string ImplementsMemberFunctionName = "implements-member"; + + /// The name of the function that indicates whether the symbol of a node is visible outside of its assembly. + public const string IsExternallyVisibleFunctionName = "is-externally-visible"; + + /// The name of the function that indicates whether the local or the parameter of a node is captured by a lambda or a local function. + public const string IsCapturedFunctionName = "is-captured"; + + /// The name of the function that returns the path of the file that is analyzed. + public const string FilePathFunctionName = "file-path"; + /// /// A context whose functions return nothing, which is used to validate a query without a compilation. /// - public static readonly BannedSyntaxXsltContext Empty = new(syntaxNavigatorFactory: null, symbolNavigatorFactory: null); + public static readonly BannedSyntaxXsltContext Empty = new(syntaxNavigatorFactory: null, symbolNavigatorFactory: null, syntaxTree: null, semanticModel: null, CancellationToken.None); private readonly Func? _syntaxNavigatorFactory; private readonly Func? _symbolNavigatorFactory; - - public BannedSyntaxXsltContext(Func? syntaxNavigatorFactory, Func? symbolNavigatorFactory) + private readonly SyntaxTree? _syntaxTree; + private readonly SemanticModel? _semanticModel; + private readonly CancellationToken _cancellationToken; + + // A query visits the same node several times, and a file usually has several queries. The context is used by a + // single analysis of a single file, which is not concurrent. The Empty context never fills them, as it has no + // semantic model. + private readonly Dictionary _nodeSymbols = new(ReferenceComparer.Instance); + private readonly Dictionary _nodeTypes = new(ReferenceComparer.Instance); + private readonly Dictionary<(string Name, string? Format), XPathTypeNameMatcher?> _typeNameMatchers = []; + private readonly Dictionary _attributeForests = new(SymbolEqualityComparer.Default); + private readonly Dictionary _memberNameMatchers = new(StringComparer.Ordinal); + private readonly Dictionary> _capturedSymbols = new(ReferenceComparer.Instance); + private string? _filePath; + + /// The navigator the syntax function returns the nodes of. + /// The navigator the symbol function returns the symbols of. + /// The file that is analyzed. + /// The semantic model of the file, which the semantic functions need. + /// The cancellation token of the analysis. + public BannedSyntaxXsltContext(Func? syntaxNavigatorFactory, Func? symbolNavigatorFactory, SyntaxTree? syntaxTree, SemanticModel? semanticModel, CancellationToken cancellationToken) : base(new NameTable()) { // The context resolves the prefixes when the expression is evaluated, so it defines the same ones as the @@ -35,10 +97,30 @@ public BannedSyntaxXsltContext(Func? syntaxNavigatorFa AddNamespace(XPathNamespaces.SymbolPrefix, XPathNamespaces.SymbolNamespaceUri); _syntaxNavigatorFactory = syntaxNavigatorFactory; _symbolNavigatorFactory = symbolNavigatorFactory; + _syntaxTree = syntaxTree; + _semanticModel = semanticModel; + _cancellationToken = cancellationToken; } public override bool Whitespace => false; + /// + /// Indicates whether a name is the one of a function that needs the semantic model, such as implements. + /// + public static bool IsSemanticFunctionName(string name) => name switch + { + ImplementsFunctionName or InheritsFromFunctionName or IsAssignableToFunctionName or HasAttributeFunctionName or AttributesFunctionName + or ContainingAssemblyFunctionName or IsFromCurrentAssemblyFunctionName or ContainingNamespaceFunctionName or OverridesFunctionName + or ImplementsMemberFunctionName or IsExternallyVisibleFunctionName or IsCapturedFunctionName => true, + _ => false, + }; + + /// + /// Indicates whether a name is the one of a function whose first argument is the name of a type, and whose + /// optional second argument is the format of this name. + /// + public static bool IsTypeNameFunctionName(string name) => name is ImplementsFunctionName or InheritsFromFunctionName or IsAssignableToFunctionName or HasAttributeFunctionName; + public override bool PreserveWhitespace(XPathNavigator node) => false; public override int CompareDocument(string baseUri, string nextbaseUri) => string.CompareOrdinal(baseUri, nextbaseUri); @@ -49,13 +131,372 @@ public BannedSyntaxXsltContext(Func? syntaxNavigatorFa // An unknown function is reported, as the engine throws when it cannot be resolved public override IXsltContextFunction ResolveFunction(string prefix, string name, XPathResultType[] argTypes) { - if (prefix.Length is 0 && string.Equals(name, SyntaxFunctionName, StringComparison.Ordinal)) - return new SyntaxFunction(_syntaxNavigatorFactory); + if (prefix.Length is not 0) + return null!; - if (prefix.Length is 0 && string.Equals(name, SymbolFunctionName, StringComparison.Ordinal)) - return new SymbolFunction(_symbolNavigatorFactory); + IXsltContextFunction? function = name switch + { + SyntaxFunctionName => new SyntaxFunction(_syntaxNavigatorFactory), + SymbolFunctionName => new SymbolFunction(_symbolNavigatorFactory), + ImplementsFunctionName => new TypeRelationFunction(this, TypeRelation.Implements), + InheritsFromFunctionName => new TypeRelationFunction(this, TypeRelation.InheritsFrom), + IsAssignableToFunctionName => new TypeRelationFunction(this, TypeRelation.IsAssignableTo), + HasAttributeFunctionName => new HasAttributeFunction(this), + AttributesFunctionName => new AttributesFunction(this), + + // A function has a single return type, so the name of the assembly and the test of the name are two functions + ContainingAssemblyFunctionName => argTypes.Length is 0 ? new ContainingAssemblyNameFunction(this) : new ContainingAssemblyTestFunction(this), + IsFromCurrentAssemblyFunctionName => new IsFromCurrentAssemblyFunction(this), + ContainingNamespaceFunctionName => argTypes.Length is 0 ? new ContainingNamespaceNameFunction(this) : new ContainingNamespaceTestFunction(this), + OverridesFunctionName => new MemberRelationFunction(this, MemberRelation.Overrides), + ImplementsMemberFunctionName => new MemberRelationFunction(this, MemberRelation.ImplementsMember), + IsExternallyVisibleFunctionName => new IsExternallyVisibleFunction(this), + IsCapturedFunctionName => new IsCapturedFunction(this), + FilePathFunctionName => new FilePathFunction(this), + _ => null, + }; + + // The engine does not check the number of arguments of the functions of a context, and a function called with + // the wrong number of arguments would silently select nothing + if (function is not null && (argTypes.Length < function.Minargs || argTypes.Length > function.Maxargs)) + { + var expected = function.Minargs == function.Maxargs + ? function.Minargs.ToString(CultureInfo.InvariantCulture) + : function.Minargs.ToString(CultureInfo.InvariantCulture) + " to " + function.Maxargs.ToString(CultureInfo.InvariantCulture); + throw new XPathException($"The '{name}' function takes {expected} argument(s), but {argTypes.Length.ToString(CultureInfo.InvariantCulture)} are given"); + } - return null!; + return function!; + } + + // The symbol of a syntax node is the one of the 'semantic:Symbol' attribute, the symbol of an operation is the one + // it refers to, and the symbol of an attribute is its class. When the navigator is on an attribute of an element, + // the symbol is the one of the element. + private ISymbol? GetContextSymbol(XPathNavigator navigator) + { + if (_semanticModel is null) + return null; + + return navigator switch + { + SyntaxNodeXPathNavigator { Node: { } node } => GetNodeSymbol(node), + OperationXPathNavigator { Operation: { } operation } => GetReferencedSymbol(operation), + SymbolXPathNavigator { Element: { } element } => element.Symbol, + AttributeDataXPathNavigator { Element: { IsAttribute: true } element } => element.Attribute.AttributeClass, + _ => null, + }; + } + + // The type of a node is the type of the expression, or the type the node declares or refers to. The type of an + // argument of an attribute is the type of its value. + private ITypeSymbol? GetContextType(XPathNavigator navigator) + { + if (_semanticModel is null) + return null; + + return navigator switch + { + SyntaxNodeXPathNavigator { Node: { } node } => GetNodeType(node), + OperationXPathNavigator { Operation: { } operation } => operation.Type, + SymbolXPathNavigator { Element: { } element } => GetTypeOfSymbol(element.Symbol), + AttributeDataXPathNavigator { Element: { } element } => element.IsAttribute ? element.Attribute.AttributeClass : element.Constant.Type, + _ => null, + }; + } + + private ISymbol? GetNodeSymbol(SyntaxNode node) + { + if (_nodeSymbols.TryGetValue(node, out var symbol)) + return symbol; + + // The syntax tree of the 'syntax' function is the one of the semantic model, but this is not the case of a + // document built without a compilation + if (node.SyntaxTree == _semanticModel!.SyntaxTree) + { + var symbolInfo = _semanticModel.GetSymbolInfo(node, _cancellationToken); + symbol = symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.FirstOrDefault() ?? _semanticModel.GetDeclaredSymbol(node, _cancellationToken); + } + + _nodeSymbols.Add(node, symbol); + return symbol; + } + + private ITypeSymbol? GetNodeType(SyntaxNode node) + { + if (_nodeTypes.TryGetValue(node, out var type)) + return type; + + if (node.SyntaxTree == _semanticModel!.SyntaxTree) + { + type = _semanticModel.GetTypeInfo(node, _cancellationToken).Type ?? GetTypeOfSymbol(GetNodeSymbol(node)); + } + + _nodeTypes.Add(node, type); + return type; + } + + private static ISymbol? GetReferencedSymbol(IOperation operation) => operation switch + { + IInvocationOperation invocation => invocation.TargetMethod, + IObjectCreationOperation objectCreation => objectCreation.Constructor, + IMemberReferenceOperation memberReference => memberReference.Member, + ILocalReferenceOperation localReference => localReference.Local, + IParameterReferenceOperation parameterReference => parameterReference.Parameter, + _ => null, + }; + + // A type is its own type, and a method has no type, as its return type is not the type of the method + private static ITypeSymbol? GetTypeOfSymbol(ISymbol? symbol) => symbol switch + { + ITypeSymbol type => type, + IFieldSymbol field => field.Type, + IPropertySymbol property => property.Type, + IEventSymbol @event => @event.Type, + IParameterSymbol parameter => parameter.Type, + ILocalSymbol local => local.Type, + _ => null, + }; + + private XPathTypeNameMatcher? GetTypeNameMatcher(object[] args) + { + if (args.Length is 0) + return null; + + var name = ToXPathString(args[0]); + var format = args.Length > 1 ? ToXPathString(args[1]) : null; + if (!_typeNameMatchers.TryGetValue((name, format), out var matcher)) + { + matcher = XPathTypeNameMatcher.Create(name, format); + _typeNameMatchers.Add((name, format), matcher); + } + + return matcher; + } + + private AttributeDataForest GetAttributeForest(ISymbol symbol) + { + if (!_attributeForests.TryGetValue(symbol, out var forest)) + { + forest = AttributeDataForest.Create(symbol, _syntaxTree, _cancellationToken); + _attributeForests.Add(symbol, forest); + } + + return forest; + } + + private XPathMemberNameMatcher? GetMemberNameMatcher(object[] args) + { + if (args is not [var value]) + return null; + + var name = ToXPathString(value); + if (!_memberNameMatchers.TryGetValue(name, out var matcher)) + { + matcher = new XPathMemberNameMatcher(name); + _memberNameMatchers.Add(name, matcher); + } + + return matcher; + } + + // The path uses '/' whatever the operating system, so a query works on every machine + private string GetFilePath() => _filePath ??= _syntaxTree?.FilePath.Replace('\\', '/') ?? ""; + + // A local or a parameter is captured when a lambda or a local function of the body that declares it uses it. The + // captured symbols are computed once per body, as the data flow analysis walks the whole body. + private bool IsCaptured(ISymbol symbol) + { + if (symbol is not (ILocalSymbol or IParameterSymbol { IsThis: false }) || symbol.ContainingSymbol is not IMethodSymbol method) + return false; + + foreach (var reference in method.DeclaringSyntaxReferences) + { + if (reference.SyntaxTree != _syntaxTree) + continue; + + var node = reference.GetSyntax(_cancellationToken); + if (!_capturedSymbols.TryGetValue(node, out var capturedSymbols)) + { + capturedSymbols = GetCapturedSymbols(node); + _capturedSymbols.Add(node, capturedSymbols); + } + + foreach (var capturedSymbol in capturedSymbols) + { + if (SymbolEqualityComparer.Default.Equals(capturedSymbol, symbol)) + return true; + } + } + + return false; + } + + private ImmutableArray GetCapturedSymbols(SyntaxNode node) + { + // The data flow analysis needs a statement or an expression, which is the body of the declaration + var body = node switch + { + BaseMethodDeclarationSyntax declaration => (SyntaxNode?)declaration.Body ?? declaration.ExpressionBody?.Expression, + LocalFunctionStatementSyntax localFunction => (SyntaxNode?)localFunction.Body ?? localFunction.ExpressionBody?.Expression, + AccessorDeclarationSyntax accessor => (SyntaxNode?)accessor.Body ?? accessor.ExpressionBody?.Expression, + AnonymousFunctionExpressionSyntax anonymousFunction => anonymousFunction.Body, + ArrowExpressionClauseSyntax arrowExpression => arrowExpression.Expression, + _ => null, + }; + + if (body is null) + { + // The locals of the top-level statements are declared by the compilation unit, and each statement is analyzed + if (node is not CompilationUnitSyntax compilationUnit) + return ImmutableArray.Empty; + + var result = ImmutableArray.CreateBuilder(); + foreach (var member in compilationUnit.Members) + { + if (member is GlobalStatementSyntax globalStatement) + { + AddCapturedSymbols(result, globalStatement.Statement); + } + } + + return result.ToImmutable(); + } + + var symbols = ImmutableArray.CreateBuilder(); + AddCapturedSymbols(symbols, body); + return symbols.ToImmutable(); + } + + private void AddCapturedSymbols(ImmutableArray.Builder symbols, SyntaxNode node) + { + var dataFlow = node switch + { + StatementSyntax statement => _semanticModel!.AnalyzeDataFlow(statement), + ExpressionSyntax expression => _semanticModel!.AnalyzeDataFlow(expression), + _ => null, + }; + + if (dataFlow is { Succeeded: true }) + { + symbols.AddRange(dataFlow.Captured); + } + } + + // A member overrides the members its overridden member overrides + private static bool Overrides(ISymbol symbol, XPathMemberNameMatcher matcher) + { + for (var overriddenMember = GetOverriddenMember(symbol); overriddenMember is not null; overriddenMember = GetOverriddenMember(overriddenMember)) + { + if (matcher.Matches(overriddenMember)) + return true; + } + + return false; + + static ISymbol? GetOverriddenMember(ISymbol symbol) => symbol switch + { + IMethodSymbol method => method.OverriddenMethod, + IPropertySymbol property => property.OverriddenProperty, + IEventSymbol @event => @event.OverriddenEvent, + _ => null, + }; + } + + // A member implements the members of the interfaces of its containing type it is the implementation of, implicitly + // or explicitly. The names are the ones of the definitions, so the definition is used for a constructed member. + private static bool ImplementsMember(ISymbol symbol, XPathMemberNameMatcher matcher) + { + symbol = symbol.OriginalDefinition; + if (symbol.Kind is not (SymbolKind.Method or SymbolKind.Property or SymbolKind.Event) || symbol.ContainingType is not { } containingType) + return false; + + foreach (var @interface in containingType.AllInterfaces) + { + foreach (var member in @interface.GetMembers()) + { + if (member.Kind == symbol.Kind && matcher.Matches(member) && SymbolEqualityComparer.Default.Equals(containingType.FindImplementationForInterfaceMember(member), symbol)) + return true; + } + } + + return false; + } + + // A symbol is visible outside of its assembly when it and all its containing types are public or protected. A + // parameter or a type parameter is visible when the symbol that declares it is, and a local never is. + private static bool IsExternallyVisible(ISymbol symbol) + { + if (symbol is IParameterSymbol or ITypeParameterSymbol) + { + symbol = symbol.ContainingSymbol; + } + + for (var current = symbol; current is not null and not INamespaceSymbol; current = current.ContainingSymbol) + { + if (current.DeclaredAccessibility is not (Accessibility.Public or Accessibility.Protected or Accessibility.ProtectedOrInternal)) + return false; + } + + return true; + } + + // The arguments are not converted to the types the function declares, so a node-set is converted to the value of + // its first node, as the 'string' function does + private static string ToXPathString(object value) => value switch + { + string text => text, + bool boolean => XPathAttributeFormatter.ToXPathBoolean(boolean), + double number => number.ToString(CultureInfo.InvariantCulture), + XPathNodeIterator iterator => iterator.MoveNext() && iterator.Current is { } current ? current.Value : "", + _ => value.ToString() ?? "", + }; + + // A type parameter has no base type and no interface of its own, so they are the ones of its constraints + private static bool InheritsFrom(ITypeSymbol type, XPathTypeNameMatcher matcher) + { + if (type is ITypeParameterSymbol typeParameter) + { + foreach (var constraintType in typeParameter.ConstraintTypes) + { + if (constraintType.TypeKind is TypeKind.Interface) + continue; + + if ((constraintType is not ITypeParameterSymbol && matcher.Matches(constraintType)) || InheritsFrom(constraintType, matcher)) + return true; + } + + return false; + } + + for (var baseType = type.BaseType; baseType is not null; baseType = baseType.BaseType) + { + if (matcher.Matches(baseType)) + return true; + } + + return false; + } + + private static bool Implements(ITypeSymbol type, XPathTypeNameMatcher matcher) + { + if (type is ITypeParameterSymbol typeParameter) + { + foreach (var constraintType in typeParameter.ConstraintTypes) + { + if ((constraintType.TypeKind is TypeKind.Interface && matcher.Matches(constraintType)) || Implements(constraintType, matcher)) + return true; + } + + return false; + } + + foreach (var @interface in type.AllInterfaces) + { + if (matcher.Matches(@interface)) + return true; + } + + return false; } private sealed class SyntaxFunction(Func? syntaxNavigatorFactory) : IXsltContextFunction @@ -98,6 +539,16 @@ public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docC } } + break; + + // An argument of an attribute has no syntax of its own, as the arguments of a 'params' array are + // several nodes, so it is the syntax of its attribute + case AttributeDataXPathNavigator { Element: { } element } navigator: + if (navigator.Forest.GetSyntaxNode(element) is { } attributeNode && visited.Add(attributeNode)) + { + nodes.Add(attributeNode); + } + break; } } @@ -153,6 +604,284 @@ public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docC } } + private enum TypeRelation + { + Implements, + InheritsFrom, + IsAssignableTo, + } + + // implements(name[, format]), inherits-from(name[, format]) and is-assignable-to(name[, format]) + private sealed class TypeRelationFunction(BannedSyntaxXsltContext context, TypeRelation relation) : IXsltContextFunction + { + public int Minargs => 1; + + public int Maxargs => 2; + + public XPathResultType ReturnType => XPathResultType.Boolean; + + public XPathResultType[] ArgTypes { get; } = [XPathResultType.String, XPathResultType.String]; + + public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext) + { + if (context.GetContextType(docContext) is not { } type || context.GetTypeNameMatcher(args) is not { } matcher) + return false; + + return relation switch + { + TypeRelation.Implements => Implements(type, matcher), + TypeRelation.InheritsFrom => InheritsFrom(type, matcher), + _ => matcher.Matches(type) || InheritsFrom(type, matcher) || Implements(type, matcher), + }; + } + } + + // has-attribute(name[, format]) is true when the class of an attribute is the type or derives from it + private sealed class HasAttributeFunction(BannedSyntaxXsltContext context) : IXsltContextFunction + { + public int Minargs => 1; + + public int Maxargs => 2; + + public XPathResultType ReturnType => XPathResultType.Boolean; + + public XPathResultType[] ArgTypes { get; } = [XPathResultType.String, XPathResultType.String]; + + public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext) + { + if (context.GetContextSymbol(docContext) is not { } symbol || context.GetTypeNameMatcher(args) is not { } matcher) + return false; + + foreach (var attribute in symbol.GetAttributes()) + { + if (attribute.AttributeClass is { } attributeClass && (matcher.Matches(attributeClass) || InheritsFrom(attributeClass, matcher))) + return true; + } + + return false; + } + } + + // attributes() returns the attributes of the symbol of the context node, and attributes(nodes) the ones of the + // symbols of the nodes, which is how a query selects them outside of a predicate + private sealed class AttributesFunction(BannedSyntaxXsltContext context) : IXsltContextFunction + { + public int Minargs => 0; + + public int Maxargs => 1; + + public XPathResultType ReturnType => XPathResultType.NodeSet; + + public XPathResultType[] ArgTypes { get; } = [XPathResultType.NodeSet]; + + public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext) + { + List? navigators = null; + if (args.Length is 0) + { + Add(docContext, visited: null); + } + else if (args is [XPathNodeIterator items]) + { + // Several nodes can refer to the same symbol, whose attributes are only returned once + var visited = new HashSet(); + while (items.MoveNext()) + { + if (items.Current is { } current) + { + Add(current, visited); + } + } + } + + return navigators is null ? NavigatorIterator.Empty : new NavigatorIterator(navigators, index: -1); + + void Add(XPathNavigator navigator, HashSet? visited) + { + if (context.GetContextSymbol(navigator) is not { } symbol) + return; + + var forest = context.GetAttributeForest(symbol); + if (forest.Roots.Length is 0 || (visited is not null && !visited.Add(forest))) + return; + + var attributeNavigator = new AttributeDataXPathNavigator(forest); + navigators ??= []; + foreach (var root in forest.Roots) + { + navigators.Add(attributeNavigator.CreateAt(root)); + } + } + } + } + + // containing-assembly() returns the name of the assembly, or an empty string when there is none + private sealed class ContainingAssemblyNameFunction(BannedSyntaxXsltContext context) : IXsltContextFunction + { + public int Minargs => 0; + + public int Maxargs => 0; + + public XPathResultType ReturnType => XPathResultType.String; + + public XPathResultType[] ArgTypes { get; } = []; + + public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext) + { + return context.GetContextSymbol(docContext)?.ContainingAssembly?.Name ?? ""; + } + } + + // containing-assembly(name) compares the names ignoring the case, as the names of the assemblies are + private sealed class ContainingAssemblyTestFunction(BannedSyntaxXsltContext context) : IXsltContextFunction + { + public int Minargs => 1; + + public int Maxargs => 1; + + public XPathResultType ReturnType => XPathResultType.Boolean; + + public XPathResultType[] ArgTypes { get; } = [XPathResultType.String]; + + public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext) + { + if (args is not [var name] || context.GetContextSymbol(docContext)?.ContainingAssembly?.Name is not { } assemblyName) + return false; + + return string.Equals(assemblyName, ToXPathString(name), StringComparison.OrdinalIgnoreCase); + } + } + + private sealed class IsFromCurrentAssemblyFunction(BannedSyntaxXsltContext context) : IXsltContextFunction + { + public int Minargs => 0; + + public int Maxargs => 0; + + public XPathResultType ReturnType => XPathResultType.Boolean; + + public XPathResultType[] ArgTypes { get; } = []; + + public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext) + { + if (context._semanticModel is null || context.GetContextSymbol(docContext)?.ContainingAssembly is not { } assembly) + return false; + + return SymbolEqualityComparer.Default.Equals(assembly, context._semanticModel.Compilation.Assembly); + } + } + + // containing-namespace() returns the namespace, such as 'System.Collections.Generic', or an empty string for the + // global namespace + private sealed class ContainingNamespaceNameFunction(BannedSyntaxXsltContext context) : IXsltContextFunction + { + public int Minargs => 0; + + public int Maxargs => 0; + + public XPathResultType ReturnType => XPathResultType.String; + + public XPathResultType[] ArgTypes { get; } = []; + + public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext) + { + return SymbolNameFormatter.GetSymbolName(context.GetContextSymbol(docContext)?.ContainingNamespace) ?? ""; + } + } + + // containing-namespace(name) is only true for this namespace, not for the namespaces it contains, like the + // namespace a type is declared in + private sealed class ContainingNamespaceTestFunction(BannedSyntaxXsltContext context) : IXsltContextFunction + { + public int Minargs => 1; + + public int Maxargs => 1; + + public XPathResultType ReturnType => XPathResultType.Boolean; + + public XPathResultType[] ArgTypes { get; } = [XPathResultType.String]; + + public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext) + { + if (args is not [var name] || context.GetContextSymbol(docContext) is not { ContainingNamespace: { } containingNamespace }) + return false; + + return string.Equals(SymbolNameFormatter.GetSymbolName(containingNamespace) ?? "", ToXPathString(name), StringComparison.Ordinal); + } + } + + private enum MemberRelation + { + Overrides, + ImplementsMember, + } + + // overrides(member) and implements-member(member) + private sealed class MemberRelationFunction(BannedSyntaxXsltContext context, MemberRelation relation) : IXsltContextFunction + { + public int Minargs => 1; + + public int Maxargs => 1; + + public XPathResultType ReturnType => XPathResultType.Boolean; + + public XPathResultType[] ArgTypes { get; } = [XPathResultType.String]; + + public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext) + { + if (context.GetContextSymbol(docContext) is not { } symbol || context.GetMemberNameMatcher(args) is not { } matcher) + return false; + + return relation is MemberRelation.Overrides ? Overrides(symbol, matcher) : ImplementsMember(symbol, matcher); + } + } + + private sealed class IsExternallyVisibleFunction(BannedSyntaxXsltContext context) : IXsltContextFunction + { + public int Minargs => 0; + + public int Maxargs => 0; + + public XPathResultType ReturnType => XPathResultType.Boolean; + + public XPathResultType[] ArgTypes { get; } = []; + + public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext) + { + return context.GetContextSymbol(docContext) is { } symbol && IsExternallyVisible(symbol); + } + } + + private sealed class IsCapturedFunction(BannedSyntaxXsltContext context) : IXsltContextFunction + { + public int Minargs => 0; + + public int Maxargs => 0; + + public XPathResultType ReturnType => XPathResultType.Boolean; + + public XPathResultType[] ArgTypes { get; } = []; + + public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext) + { + return context.GetContextSymbol(docContext) is { } symbol && context.IsCaptured(symbol); + } + } + + // file-path() does not depend on the node, so it does not need the semantic model + private sealed class FilePathFunction(BannedSyntaxXsltContext context) : IXsltContextFunction + { + public int Minargs => 0; + + public int Maxargs => 0; + + public XPathResultType ReturnType => XPathResultType.String; + + public XPathResultType[] ArgTypes { get; } = []; + + public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext) => context.GetFilePath(); + } + // The navigators of the list are never moved, as each iterator exposes its own copy of the current one private sealed class NavigatorIterator : XPathNodeIterator { diff --git a/src/Meziantou.Analyzer/Internals/XPathMemberNameMatcher.cs b/src/Meziantou.Analyzer/Internals/XPathMemberNameMatcher.cs new file mode 100644 index 000000000..42810ef19 --- /dev/null +++ b/src/Meziantou.Analyzer/Internals/XPathMemberNameMatcher.cs @@ -0,0 +1,32 @@ +namespace Meziantou.Analyzer.Internals; + +/// +/// Matches a member against the name a query gives to the overrides and implements-member functions. +/// The name is either the documentation comment id of the member, such as M:System.Object.Equals(System.Object), +/// which selects a single overload, or its name qualified by the metadata name of its containing type, such as +/// System.Object.Equals, which selects all the overloads. A qualified name cannot contain ':', so the format +/// is detected from the name. +/// +internal sealed class XPathMemberNameMatcher +{ + private readonly string _name; + private readonly bool _isDocumentationId; + + public XPathMemberNameMatcher(string name) + { + _name = name; + _isDocumentationId = name.Length >= 2 && char.IsLetter(name[0]) && name[1] is ':'; + } + + public bool Matches(ISymbol member) + { + // The name of the member is part of both formats, so it is tested before formatting the full name, as most + // of the members a query visits do not match. The documentation comment id uses the metadata name, which is + // 'Item' for an indexer whose name is 'this[]'. + if (!_name.Contains(_isDocumentationId ? member.MetadataName : member.Name, StringComparison.Ordinal)) + return false; + + var name = _isDocumentationId ? SymbolNameFormatter.GetDocumentationId(member) : SymbolNameFormatter.GetSymbolName(member); + return string.Equals(name, _name, StringComparison.Ordinal); + } +} diff --git a/src/Meziantou.Analyzer/Internals/XPathPropertyAttributes.cs b/src/Meziantou.Analyzer/Internals/XPathPropertyAttributes.cs index a3161d193..4dbeacb09 100644 --- a/src/Meziantou.Analyzer/Internals/XPathPropertyAttributes.cs +++ b/src/Meziantou.Analyzer/Internals/XPathPropertyAttributes.cs @@ -322,10 +322,10 @@ private ReflectedProperty[] GetProperties(Type type) return null; } - private static TypeAttributeNames CreateTypeNames(string name) + internal static TypeAttributeNames CreateTypeNames(string name) => new(name + "Name", name + "MetadataName", name + "DocumentationId", name + "ReferenceId", name + "Kind", name + "IsValueType", name + "NullableAnnotation", name + "SpecialType"); - private static SymbolAttributeNames CreateSymbolNames(string name) + internal static SymbolAttributeNames CreateSymbolNames(string name) => new(name, name + "Name", name + "DocumentationId", name + "Kind", name + "IsStatic", name + "IsAbstract", name + "IsVirtual", name + "IsOverride", name + "IsSealed", name + "IsAsync", name + "IsExtensionMethod", name + "Arity", name + "RefKind", name + "IsParams", name + "IsOptional", name + "IsConst", name + "IsReadOnly"); private static ConversionAttributeNames CreateConversionNames(string name) diff --git a/src/Meziantou.Analyzer/Internals/XPathTypeNameMatcher.cs b/src/Meziantou.Analyzer/Internals/XPathTypeNameMatcher.cs new file mode 100644 index 000000000..ba57ef314 --- /dev/null +++ b/src/Meziantou.Analyzer/Internals/XPathTypeNameMatcher.cs @@ -0,0 +1,142 @@ +namespace Meziantou.Analyzer.Internals; + +/// +/// Matches a type against the name a query gives to the implements, inherits-from, +/// is-assignable-to and has-attribute functions. The name is in one of the formats of the attributes +/// that expose a type, so it can be copied from one of them. +/// +internal sealed class XPathTypeNameMatcher +{ + /// The name of the format of System.Collections.Generic.List`1. + public const string MetadataNameFormat = "MetadataName"; + + /// The name of the format of T:System.Collections.Generic.List`1. + public const string DocumentationDeclarationIdFormat = "DocumentationDeclarationId"; + + /// The name of the format of System.Collections.Generic.List{System.String}. + public const string DocumentationReferenceIdFormat = "DocumentationReferenceId"; + + private readonly string _name; + private readonly TypeNameFormat _format; + + // The name of the type alone, which is compared before formatting the full name of a type, as most of the types + // a query visits do not match + private readonly string _simpleName; + + private XPathTypeNameMatcher(string name, TypeNameFormat format) + { + _name = name; + _format = format; + _simpleName = GetSimpleName(name, format); + } + + /// + /// Indicates whether a value is the name of a format. + /// + public static bool IsFormatName(string value) => TryParseFormat(value, out _); + + /// + /// Creates a matcher for a name. The format is detected from the name when it is not given. It is null when the + /// format is not valid, so the name matches nothing. + /// + public static XPathTypeNameMatcher? Create(string name, string? format) + { + if (format is null) + return new XPathTypeNameMatcher(name, DetectFormat(name)); + + return TryParseFormat(format, out var parsedFormat) ? new XPathTypeNameMatcher(name, parsedFormat) : null; + } + + public bool Matches(ITypeSymbol type) + { + // The name of an array or of a pointer is empty, so it is not compared first + if (type is INamedTypeSymbol namedType && !string.Equals(_format is TypeNameFormat.DocumentationReferenceId ? namedType.Name : namedType.MetadataName, _simpleName, StringComparison.Ordinal)) + return false; + + var name = _format switch + { + TypeNameFormat.MetadataName => SymbolNameFormatter.GetMetadataName(type.OriginalDefinition), + TypeNameFormat.DocumentationDeclarationId => SymbolNameFormatter.GetDocumentationId(type), + _ => SymbolNameFormatter.GetReferenceId(type), + }; + + return string.Equals(name, _name, StringComparison.Ordinal); + } + + // A documentation comment id starts with the kind of the symbol, such as 'T:', and a reference id is the only + // format that has type arguments + private static TypeNameFormat DetectFormat(string name) + { + if (name.Length >= 2 && char.IsLetter(name[0]) && name[1] is ':') + return TypeNameFormat.DocumentationDeclarationId; + + if (name.Contains('{', StringComparison.Ordinal)) + return TypeNameFormat.DocumentationReferenceId; + + return TypeNameFormat.MetadataName; + } + + private static bool TryParseFormat(string value, out TypeNameFormat format) + { + switch (value) + { + case MetadataNameFormat: + format = TypeNameFormat.MetadataName; + return true; + + case DocumentationDeclarationIdFormat: + format = TypeNameFormat.DocumentationDeclarationId; + return true; + + case DocumentationReferenceIdFormat: + format = TypeNameFormat.DocumentationReferenceId; + return true; + + default: + format = default; + return false; + } + } + + // The metadata name of a type is the last part of its metadata name and of its documentation comment id, such as + // 'List`1', whereas the reference id has the name of the type without the arity and followed by its type + // arguments, such as 'List{System.String}', which can contain '.' too + private static string GetSimpleName(string name, TypeNameFormat format) + { + var start = 0; + var end = name.Length; + var depth = 0; + for (var i = 0; i < name.Length; i++) + { + switch (name[i]) + { + case '{' when format is TypeNameFormat.DocumentationReferenceId: + if (depth is 0) + { + end = i; + } + + depth++; + break; + + case '}' when format is TypeNameFormat.DocumentationReferenceId: + depth--; + break; + + case '.' or '+' or ':' when depth is 0: + start = i + 1; + end = name.Length; + break; + } + } + + return end > start ? name.Substring(start, end - start) : ""; + } + + private enum TypeNameFormat + { + MetadataName, + DocumentationDeclarationId, + DocumentationReferenceId, + } +} diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseBannedSyntaxAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseBannedSyntaxAnalyzer.cs index 5e3bef94f..a69fa13e5 100644 --- a/src/Meziantou.Analyzer/Rules/DoNotUseBannedSyntaxAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseBannedSyntaxAnalyzer.cs @@ -149,8 +149,8 @@ private static ParsedQuery GetQuery(string query) { try { - // The context defines the 'semantic', 'operation' and 'symbol' prefixes and the 'syntax' and 'symbol' - // functions. An undefined prefix throws when the query is compiled, and an expression that is not + // The context defines the 'semantic', 'operation' and 'symbol' prefixes, the 'syntax' and 'symbol' + // functions, and the semantic functions, such as 'implements'. An undefined prefix throws when the query is compiled, and an expression that is not // compiled with an XsltContext cannot use a function of its own, whatever the context it is evaluated with. var expression = XPathExpression.Compile(query, BannedSyntaxXsltContext.Empty); if (expression.ReturnType is not XPathResultType.NodeSet) @@ -196,6 +196,7 @@ private static (BannedSyntaxTarget Target, string? ErrorMessage, HashSet var usesSymbols = false; var usesSyntaxFunction = false; var usesSymbolFunction = false; + var usesSemanticFunction = false; // null once the query selects attributes it does not name, so all of them must be computed var attributeNames = new HashSet(StringComparer.Ordinal); @@ -262,6 +263,14 @@ private static (BannedSyntaxTarget Target, string? ErrorMessage, HashSet { usesSymbolFunction = true; } + else if (BannedSyntaxXsltContext.IsSemanticFunctionName(query.Substring(start, end - start))) + { + usesSemanticFunction = true; + + // An unknown format would silently match nothing, so it is reported when it is a literal + if (BannedSyntaxXsltContext.IsTypeNameFunctionName(query.Substring(start, end - start)) && GetSecondStringArgument(query, end) is { } format && !XPathTypeNameMatcher.IsFormatName(format)) + return (BannedSyntaxTarget.SemanticSyntax, $"'{format}' is not a valid type name format. The valid formats are '{XPathTypeNameMatcher.MetadataNameFormat}', '{XPathTypeNameMatcher.DocumentationDeclarationIdFormat}' and '{XPathTypeNameMatcher.DocumentationReferenceIdFormat}'", AttributeNames: null); + } else if (IsAttributeName(query, start)) { // '@node()' selects the attributes the query does not name @@ -359,7 +368,69 @@ private static (BannedSyntaxTarget Target, string? ErrorMessage, HashSet return (BannedSyntaxTarget.Operations, null, attributeNames); } - return (usesSemanticModel ? BannedSyntaxTarget.SemanticSyntax : BannedSyntaxTarget.Syntax, null, attributeNames); + // The semantic functions, such as 'implements', need the semantic model, which the operations and the symbols + // already have + return (usesSemanticModel || usesSemanticFunction ? BannedSyntaxTarget.SemanticSyntax : BannedSyntaxTarget.Syntax, null, attributeNames); + } + + // The second argument of the function call whose name ends at this index, when it is a string literal, such as + // the format of 'implements('System.IDisposable', 'MetadataName')'. The arguments can contain function calls, + // predicates and literals, so only the ',' that are not nested separate the arguments. + private static string? GetSecondStringArgument(string query, int index) + { + while (index < query.Length && query[index] is not '(') + { + index++; + } + + var depth = 0; + var quote = '\0'; + for (var i = index + 1; i < query.Length; i++) + { + var c = query[i]; + if (quote is not '\0') + { + if (c == quote) + { + quote = '\0'; + } + + continue; + } + + switch (c) + { + case '\'' or '"': + quote = c; + break; + + case '(' or '[': + depth++; + break; + + case ')' or ']': + if (depth is 0) + return null; + + depth--; + break; + + case ',' when depth is 0: + var start = i + 1; + while (start < query.Length && char.IsWhiteSpace(query[start])) + { + start++; + } + + if (start >= query.Length || query[start] is not ('\'' or '"')) + return null; + + var end = query.IndexOf(query[start], start + 1, StringComparison.Ordinal); + return end < 0 ? null : query.Substring(start + 1, end - start - 1); + } + } + + return null; } // The name is preceded by '@', so it is the name of an attribute and not the name of an element @@ -739,7 +810,7 @@ private void Analyze(SyntaxNode root, SemanticModel? semanticModel, ReportSink s if (_queries.Count > 0) { var forest = new SyntaxForest(root, semanticModel: null, _queryFilter, cancellationToken); - Evaluate(_queries, new SyntaxNodeXPathNavigator(forest), sink); + Evaluate(_queries, new SyntaxNodeXPathNavigator(forest), sink, new BannedSyntaxXsltContext(syntaxNavigatorFactory: null, symbolNavigatorFactory: null, root.SyntaxTree, semanticModel: null, cancellationToken)); } if (_semanticQueries.Count > 0 && semanticModel is not null) @@ -750,7 +821,7 @@ private void Analyze(SyntaxNode root, SemanticModel? semanticModel, ReportSink s // The symbols are only built when an entry uses the 'symbol' function. The 'syntax' function goes back // to the same tree, so the semantic attributes are still available after a round trip. SymbolXPathNavigator? symbolNavigator = null; - var context = new BannedSyntaxXsltContext(() => navigator, () => symbolNavigator ??= new SymbolXPathNavigator(SymbolForest.Create(root, semanticModel, _semanticQueryFilter, cancellationToken))); + var context = new BannedSyntaxXsltContext(() => navigator, () => symbolNavigator ??= new SymbolXPathNavigator(SymbolForest.Create(root, semanticModel, _semanticQueryFilter, cancellationToken)), root.SyntaxTree, semanticModel, cancellationToken); Evaluate(_semanticQueries, navigator, sink, context); } @@ -762,7 +833,7 @@ private void Analyze(SyntaxNode root, SemanticModel? semanticModel, ReportSink s // The navigator of the syntax tree is only built when an entry uses the 'syntax' function. It has no // semantic model, as the semantic attributes are not available in a query on the operations. SyntaxNodeXPathNavigator? syntaxNavigator = null; - var context = new BannedSyntaxXsltContext(() => syntaxNavigator ??= new SyntaxNodeXPathNavigator(new SyntaxForest(root, semanticModel: null, _operationQueryFilter, cancellationToken)), symbolNavigatorFactory: null); + var context = new BannedSyntaxXsltContext(() => syntaxNavigator ??= new SyntaxNodeXPathNavigator(new SyntaxForest(root, semanticModel: null, _operationQueryFilter, cancellationToken)), symbolNavigatorFactory: null, root.SyntaxTree, semanticModel, cancellationToken); Evaluate(_operationQueries, new OperationXPathNavigator(forest, cancellationToken), sink, context); } @@ -774,7 +845,7 @@ private void Analyze(SyntaxNode root, SemanticModel? semanticModel, ReportSink s // Like for the operations, the navigator of the syntax tree is only built when an entry uses the // 'syntax' function, and it has no semantic model SyntaxNodeXPathNavigator? syntaxNavigator = null; - var context = new BannedSyntaxXsltContext(() => syntaxNavigator ??= new SyntaxNodeXPathNavigator(new SyntaxForest(root, semanticModel: null, _symbolQueryFilter, cancellationToken)), symbolNavigatorFactory: null); + var context = new BannedSyntaxXsltContext(() => syntaxNavigator ??= new SyntaxNodeXPathNavigator(new SyntaxForest(root, semanticModel: null, _symbolQueryFilter, cancellationToken)), symbolNavigatorFactory: null, root.SyntaxTree, semanticModel, cancellationToken); Evaluate(_symbolQueries, new SymbolXPathNavigator(forest), sink, context); } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBannedSyntaxAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBannedSyntaxAnalyzerTests.cs index 0cd475a39..3fc9c9498 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBannedSyntaxAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBannedSyntaxAnalyzerTests.cs @@ -3453,4 +3453,909 @@ class Sample return test.RunAsync(); } + + [Fact] + public Task Implements_Symbol() + { + var test = CreateTest("//symbol:NamedType[implements('System.IDisposable')]"); + test.TestCode = """ + using System; + + class {|#0:A|} : IDisposable { public void Dispose() { } } + class {|#1:B|} : A { } + interface {|#2:I|} : IDisposable { } + class C { } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:NamedType", "")); + test.ExpectedDiagnostics.Add(Diagnostic(1, "symbol:NamedType", "")); + test.ExpectedDiagnostics.Add(Diagnostic(2, "symbol:NamedType", "")); + + return test.RunAsync(); + } + + [Fact] + public Task Implements_TypeItselfIsExcluded() + { + var test = CreateTest("//IdentifierName[implements('System.IDisposable')]"); + test.TestCode = """ + class Sample + { + System.IDisposable a; + } + """; + + return test.RunAsync(); + } + + [Theory] + [InlineData("//ObjectCreationExpression[implements('System.IDisposable')]", "ObjectCreationExpression")] + [InlineData("//operation:ObjectCreation[implements('T:System.IDisposable')]", "operation:ObjectCreation")] + public Task Implements_TypeOfTheExpression(string query, string name) + { + var test = CreateTest(query); + test.TestCode = """ + class Sample + { + void M() + { + _ = {|#0:new System.IO.MemoryStream()|}; + _ = new object(); + } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, name, "")); + + return test.RunAsync(); + } + + [Fact] + public Task Implements_TypeParameterConstraint() + { + var test = CreateTest("//symbol:Parameter[implements('System.IDisposable')]"); + test.TestCode = """ + class Sample + { + void M(T {|#0:a|}, U b) where T : System.IDisposable { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Parameter", "")); + + return test.RunAsync(); + } + + [Fact] + public Task InheritsFrom_BaseClasses() + { + var test = CreateTest("//symbol:NamedType[inherits-from('N.Base')]"); + test.TestCode = """ + namespace N; + + class Base { } + class {|#0:Derived|} : Base { } + class {|#1:Derived2|} : Derived { } + interface I { } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:NamedType", "")); + test.ExpectedDiagnostics.Add(Diagnostic(1, "symbol:NamedType", "")); + + return test.RunAsync(); + } + + [Fact] + public Task InheritsFrom_InterfaceIsNotABaseClass() + { + var test = CreateTest("//symbol:NamedType[inherits-from('System.IDisposable')]"); + test.TestCode = """ + class Sample : System.IDisposable { public void Dispose() { } } + """; + + return test.RunAsync(); + } + + [Fact] + public Task InheritsFrom_NestedType() + { + var test = CreateTest("//symbol:NamedType[inherits-from('Outer+Base')]"); + test.TestCode = """ + class Outer + { + public class Base { } + } + + class {|#0:Derived|} : Outer.Base { } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:NamedType", "")); + + return test.RunAsync(); + } + + [Fact] + public Task IsAssignableTo_GenericDefinition() + { + var test = CreateTest("//symbol:Parameter[is-assignable-to('System.Collections.Generic.IEnumerable`1')]"); + test.TestCode = """ + using System.Collections.Generic; + + class Sample + { + void M(List {|#0:a|}, IEnumerable {|#1:b|}, string {|#2:c|}, int d) { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Parameter", "")); + test.ExpectedDiagnostics.Add(Diagnostic(1, "symbol:Parameter", "")); + test.ExpectedDiagnostics.Add(Diagnostic(2, "symbol:Parameter", "")); + + return test.RunAsync(); + } + + [Fact] + public Task IsAssignableTo_ReferenceId_SelectsTheTypeArguments() + { + var test = CreateTest("//symbol:Parameter[is-assignable-to('System.Collections.Generic.IEnumerable{System.String}')]"); + test.TestCode = """ + using System.Collections.Generic; + + class Sample + { + void M(List {|#0:a|}, List b, string[] {|#1:c|}) { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Parameter", "")); + test.ExpectedDiagnostics.Add(Diagnostic(1, "symbol:Parameter", "")); + + return test.RunAsync(); + } + + [Theory] + [InlineData("System.String", "MetadataName")] + [InlineData("T:System.String", "DocumentationDeclarationId")] + [InlineData("System.String", "DocumentationReferenceId")] + public Task IsAssignableTo_ExplicitFormat(string name, string format) + { + var test = CreateTest($"//symbol:Parameter[is-assignable-to('{name}', '{format}')]"); + test.TestCode = """ + class Sample + { + void M(string {|#0:a|}, object b) { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Parameter", "")); + + return test.RunAsync(); + } + + [Fact] + public Task IsAssignableTo_ExplicitFormatDoesNotMatchTheOtherFormats() + { + var test = CreateTest("//symbol:Parameter[is-assignable-to('T:System.String', 'MetadataName')]"); + test.TestCode = """ + class Sample + { + void M(string a) { } + } + """; + + return test.RunAsync(); + } + + [Fact] + public Task HasAttribute_DerivedAttributeClass() + { + var test = CreateTest("//symbol:Method[has-attribute('BaseAttribute')]"); + test.TestCode = """ + using System; + + class BaseAttribute : Attribute { } + class DerivedAttribute : BaseAttribute { } + + class Sample + { + [Base] void {|#0:A|}() { } + [Derived] void {|#1:B|}() { } + void C() { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Method", "")); + test.ExpectedDiagnostics.Add(Diagnostic(1, "symbol:Method", "")); + + return test.RunAsync(); + } + + [Theory] + [InlineData("//InvocationExpression[has-attribute('T:System.ObsoleteAttribute')]", "InvocationExpression")] + [InlineData("//operation:Invocation[has-attribute('System.ObsoleteAttribute')]", "operation:Invocation")] + public Task HasAttribute_CalledMethod(string query, string name) + { + var test = CreateTest(query); + test.TestCode = """ + class Sample + { + [System.Obsolete] + void Old() { } + + void New() { } + + void M() + { + {|#0:Old()|}; + New(); + } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, name, "")); + + return test.RunAsync(); + } + + [Fact] + public Task Attributes_ConstructorArgument() + { + var test = CreateTest("//symbol:Method[attributes()[@AttributeClassName='ObsoleteAttribute']/ConstructorArgument[@Position='0' and @Value='Use N']]"); + test.TestCode = """ + using System; + + class Sample + { + [Obsolete("Use N")] void {|#0:M|}() { } + [Obsolete("Other")] void O() { } + void N() { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Method", "")); + + return test.RunAsync(); + } + + [Fact] + public Task Attributes_EmptyStringHasNoValue() + { + var test = CreateTest("//symbol:Method[attributes()/ConstructorArgument[@IsNull='false' and not(@Value)]]"); + test.TestCode = """ + using System; + + class Sample + { + [Obsolete("")] void {|#0:M|}() { } + [Obsolete(null)] void N() { } + [Obsolete("a")] void O() { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Method", "")); + + return test.RunAsync(); + } + + [Fact] + public Task Attributes_NamedArgument() + { + var test = CreateTest("//symbol:NamedType[attributes()/NamedArgument[@Name='AllowMultiple' and @Value='true']]"); + test.TestCode = """ + using System; + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + class {|#0:AAttribute|} : Attribute { } + + [AttributeUsage(AttributeTargets.All)] + class BAttribute : Attribute { } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:NamedType", "")); + + return test.RunAsync(); + } + + [Fact] + public Task Attributes_ArrayItems() + { + var test = CreateTest("//symbol:Method[attributes()/ConstructorArgument/Item[@Value='2']]"); + test.TestCode = """ + using System; + + class MyAttribute : Attribute + { + public MyAttribute(params int[] values) { } + } + + class Sample + { + [My(1, 2)] void {|#0:M|}() { } + [My(3)] void N() { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Method", "")); + + return test.RunAsync(); + } + + [Fact] + public Task Attributes_TypeArgument() + { + var test = CreateTest("//symbol:Method[attributes()/ConstructorArgument[@Kind='Type' and @Value='System.String']]"); + test.TestCode = """ + using System; + + class MyAttribute : Attribute + { + public MyAttribute(Type type) { } + } + + class Sample + { + [My(typeof(string))] void {|#0:M|}() { } + [My(typeof(int))] void N() { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Method", "")); + + return test.RunAsync(); + } + + [Fact] + public Task Attributes_ReportedOnTheAttribute() + { + var test = CreateTest("attributes(//symbol:Method)[@AttributeClassMetadataName='System.ObsoleteAttribute']"); + test.TestCode = """ + using System; + + class Sample + { + [{|#0:Obsolete|}, CLSCompliant(false)] + void M() { } + + [{|#1:Obsolete("a")|}] + void N() { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "AttributeData", "")); + test.ExpectedDiagnostics.Add(Diagnostic(1, "AttributeData", "")); + + return test.RunAsync(); + } + + [Fact] + public Task Attributes_SyntaxFunction() + { + var test = CreateTest("syntax(attributes(//symbol:Method))"); + test.TestCode = """ + class Sample + { + [{|#0:System.Obsolete|}] + void M() { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "Attribute", "")); + + return test.RunAsync(); + } + + [Fact] + public Task Attributes_FromMetadata_NotReported() + { + var test = CreateTest("attributes(//IdentifierName[@Identifier='Obsolete' and @semantic:SymbolName='ObsoleteAttribute'])"); + test.TestCode = """ + class Sample + { + [System.Obsolete] + void M() { } + } + """; + + return test.RunAsync(); + } + + [Fact] + public Task ContainingAssembly_Name() + { + var test = CreateTest("//InvocationExpression[starts-with(containing-assembly(), 'System.')]"); + test.TestCode = """ + class Sample + { + void M() + { + {|#0:System.Console.WriteLine()|}; + N(); + } + + void N() { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "InvocationExpression", "")); + + return test.RunAsync(); + } + + [Fact] + public Task ContainingAssembly_Test_IgnoresTheCase() + { + var test = CreateTest("//operation:Invocation[containing-assembly('system.console')]"); + test.TestCode = """ + class Sample + { + void M() + { + {|#0:System.Console.WriteLine()|}; + N(); + } + + void N() { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "operation:Invocation", "")); + + return test.RunAsync(); + } + + [Fact] + public Task IsFromCurrentAssembly() + { + var test = CreateTest("//InvocationExpression[not(is-from-current-assembly())]"); + test.TestCode = """ + class Sample + { + void M() + { + {|#0:System.Console.WriteLine()|}; + N(); + } + + void N() { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "InvocationExpression", "")); + + return test.RunAsync(); + } + + [Fact] + public Task IsFromCurrentAssembly_Symbols() + { + var test = CreateTest("//symbol:NamedType[is-from-current-assembly()]"); + test.TestCode = """ + class {|#0:Sample|} + { + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:NamedType", "")); + + return test.RunAsync(); + } + + [Fact] + public Task SemanticFunction_OnTheSyntaxOfAnOperation() + { + var test = CreateTest("syntax(//operation:Invocation)[has-attribute('System.ObsoleteAttribute')]"); + test.TestCode = """ + class Sample + { + [System.Obsolete] + void Old() { } + + void M() + { + {|#0:Old()|}; + M(); + } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "InvocationExpression", "")); + + return test.RunAsync(); + } + + [Theory] + [InlineData("//GotoStatement[contains(file-path(), '/Migrations/')]")] + [InlineData("//GotoStatement[substring(file-path(), string-length(file-path()) - 8) = 'Legacy.cs']")] + public Task FilePath_SyntaxQuery(string query) + { + var test = CreateTest(query); + test.TestState.Sources.Add(("/src/Migrations/Legacy.cs", """ + class A + { + void M() + { + {|#0:goto end;|} + end: + return; + } + } + """)); + test.TestState.Sources.Add(("/src/B.cs", """ + class B + { + void M() + { + goto end; + end: + return; + } + } + """)); + test.ExpectedDiagnostics.Add(Diagnostic(0, "GotoStatement", "")); + + return test.RunAsync(); + } + + [Fact] + public Task FilePath_SymbolQuery() + { + var test = CreateTest("//symbol:NamedType[not(contains(file-path(), '/Tests/'))]"); + test.TestState.Sources.Add(("/src/Tests/A.cs", """ + class A { } + """)); + test.TestState.Sources.Add(("/src/B.cs", """ + class {|#0:B|} { } + """)); + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:NamedType", "")); + + return test.RunAsync(); + } + + [Theory] + [InlineData("//symbol:Method[overrides('M:System.Object.ToString')]")] + [InlineData("//symbol:Method[overrides('System.Object.ToString')]")] + public Task Overrides_Transitively(string query) + { + var test = CreateTest(query); + test.TestCode = """ + class A + { + public override string {|#0:ToString|}() => ""; + } + + class B : A + { + public override string {|#1:ToString|}() => ""; + public override int GetHashCode() => 0; + } + + class C + { + public new string ToString() => ""; + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Method", "")); + test.ExpectedDiagnostics.Add(Diagnostic(1, "symbol:Method", "")); + + return test.RunAsync(); + } + + [Fact] + public Task Overrides_DocumentationIdSelectsASingleOverload() + { + var test = CreateTest("//MethodDeclaration[overrides('M:System.Object.Equals(System.Object)')]/@Identifier"); + test.TestCode = """ + class Sample + { + public override bool {|#0:Equals|}(object obj) => false; + public bool Equals(Sample other) => false; + public override int GetHashCode() => 0; + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "MethodDeclaration/@Identifier", "")); + + return test.RunAsync(); + } + + [Fact] + public Task Overrides_Property() + { + var test = CreateTest("//symbol:Property[overrides('P:A.Value')]"); + test.TestCode = """ + abstract class A + { + public abstract int Value { get; } + } + + class B : A + { + public override int {|#0:Value|} => 0; + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Property", "")); + + return test.RunAsync(); + } + + [Fact] + public Task ImplementsMember_ImplicitAndExplicit() + { + var test = CreateTest("//symbol:Method[implements-member('M:System.IDisposable.Dispose')]"); + test.TestCode = """ + using System; + + class A : IDisposable + { + public void {|#0:Dispose|}() { } + } + + class B : IDisposable + { + void IDisposable.{|#1:Dispose|}() { } + public void Dispose(bool disposing) { } + } + + class C + { + public void Dispose() { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Method", "")); + test.ExpectedDiagnostics.Add(Diagnostic(1, "symbol:Method", "")); + + return test.RunAsync(); + } + + [Fact] + public Task ImplementsMember_GenericInterface() + { + var test = CreateTest("//symbol:Method[implements-member('System.IEquatable`1.Equals')]"); + test.TestCode = """ + class Sample : System.IEquatable + { + public bool {|#0:Equals|}(Sample other) => false; + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Method", "")); + + return test.RunAsync(); + } + + [Fact] + public Task ImplementsMember_Invocation() + { + var test = CreateTest("//operation:Invocation[implements-member('M:System.IDisposable.Dispose')]"); + test.TestCode = """ + class Sample + { + void M(System.IO.MemoryStream stream) + { + {|#0:stream.Dispose()|}; + stream.Flush(); + } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "operation:Invocation", "")); + + return test.RunAsync(); + } + + [Theory] + [InlineData("//InvocationExpression[containing-namespace('System.IO')]")] + [InlineData("//InvocationExpression[starts-with(containing-namespace(), 'System.I')]")] + public Task ContainingNamespace(string query) + { + var test = CreateTest(query); + test.TestCode = """ + namespace N; + + class Sample + { + void M() + { + {|#0:System.IO.File.Exists("")|}; + System.Console.WriteLine(); + System.Collections.Generic.EqualityComparer.Default.GetHashCode(0); + M(); + } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "InvocationExpression", "")); + + return test.RunAsync(); + } + + [Fact] + public Task ContainingNamespace_GlobalNamespaceIsEmpty() + { + var test = CreateTest("//symbol:NamedType[containing-namespace() = '']"); + test.TestCode = """ + class {|#0:A|} { } + + namespace N + { + class B { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:NamedType", "")); + + return test.RunAsync(); + } + + [Fact] + public Task IsExternallyVisible() + { + var test = CreateTest("//symbol:Method[is-externally-visible()]"); + test.TestCode = """ + public class A + { + public void {|#0:M1|}() { } + protected void {|#1:M2|}() { } + protected internal void {|#2:M3|}() { } + private protected void M4() { } + internal void M5() { } + void M6() { } + } + + internal class B + { + public void M() { } + } + + public class C + { + private class D + { + public void M() { } + } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Method", "")); + test.ExpectedDiagnostics.Add(Diagnostic(1, "symbol:Method", "")); + test.ExpectedDiagnostics.Add(Diagnostic(2, "symbol:Method", "")); + + return test.RunAsync(); + } + + [Fact] + public Task IsExternallyVisible_Parameter() + { + var test = CreateTest("//symbol:Parameter[is-externally-visible()]"); + test.TestCode = """ + public class A + { + public void M(int {|#0:a|}) { } + internal void N(int b) { } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Parameter", "")); + + return test.RunAsync(); + } + + [Fact] + public Task IsCaptured_Symbols() + { + var test = CreateTest("//symbol:*[is-captured()]"); + test.TestCode = """ + using System; + + class Sample + { + void M(int {|#0:a|}, int b) + { + var {|#1:c|} = 0; + var {|#2:d|} = b; + Func f = () => a + c; + int Local() => d; + Func g = x => x; + } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Parameter", "")); + test.ExpectedDiagnostics.Add(Diagnostic(1, "symbol:Local", "")); + test.ExpectedDiagnostics.Add(Diagnostic(2, "symbol:Local", "")); + + return test.RunAsync(); + } + + [Fact] + public Task IsCaptured_LocalFunction() + { + var test = CreateTest("//symbol:Local[is-captured()]"); + test.TestCode = """ + class Sample + { + void M() + { + var {|#0:a|} = 0; + var b = 0; + int Local() => a; + static int Static(int b) => b; + } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Local", "")); + + return test.RunAsync(); + } + + [Fact] + public Task IsCaptured_References() + { + var test = CreateTest("//operation:LocalReference[is-captured()]"); + test.TestCode = """ + class Sample + { + void M() + { + var a = 0; + System.Func f = () => {|#0:a|}; + {|#1:a|}++; + } + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "operation:LocalReference", "")); + test.ExpectedDiagnostics.Add(Diagnostic(1, "operation:LocalReference", "")); + + return test.RunAsync(); + } + + [Fact] + public Task IsCaptured_NestedLambda() + { + var test = CreateTest("//symbol:Local[is-captured()]"); + test.TestCode = """ + using System; + + class Sample + { + Func> F = () => + { + var {|#0:a|} = 0; + var b = 0; + return () => a; + }; + } + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Local", "")); + + return test.RunAsync(); + } + + [Fact] + public Task IsCaptured_TopLevelStatements() + { + var test = CreateTest("//symbol:Local[is-captured()]"); + test.TestState.OutputKind = OutputKind.ConsoleApplication; + test.TestCode = """ + var {|#0:a|} = 0; + var b = 0; + System.Func f = () => a; + """; + test.ExpectedDiagnostics.Add(Diagnostic(0, "symbol:Local", "")); + + return test.RunAsync(); + } + + [Theory] + [InlineData("//symbol:NamedType[implements('System.IDisposable', 'Typo')]", "'Typo' is not a valid type name format. The valid formats are 'MetadataName', 'DocumentationDeclarationId' and 'DocumentationReferenceId'")] + [InlineData("//ClassDeclaration[has-attribute(concat('a', 'b'), \"typo\")]", "'typo' is not a valid type name format. The valid formats are 'MetadataName', 'DocumentationDeclarationId' and 'DocumentationReferenceId'")] + public Task InvalidEntry_UnknownTypeNameFormat_Reported(string query, string message) + { + var test = new AnalyzerTest { MarkupOptions = MarkupOptions.UseFirstDescriptor }; + test.TestState.AdditionalFiles.Add(("BannedSyntaxes.txt", "{|#0:" + query + "|}")); + test.TestCode = """ + class Sample + { + } + """; + test.ExpectedDiagnostics.Add(new DiagnosticResult("MA0241", DiagnosticSeverity.Warning).WithLocation(0).WithArguments(query, message)); + + return test.RunAsync(); + } + + [Theory] + [InlineData("//symbol:NamedType[implements()]")] + [InlineData("//symbol:NamedType[implements('a', 'MetadataName', 'b')]")] + [InlineData("//symbol:NamedType[containing-assembly('a', 'b')]")] + [InlineData("//symbol:NamedType[is-from-current-assembly('a')]")] + [InlineData("attributes(//symbol:NamedType, //symbol:Method)")] + [InlineData("//symbol:Method[overrides()]")] + [InlineData("//symbol:Method[implements-member('a', 'b')]")] + [InlineData("//symbol:Method[containing-namespace('a', 'b')]")] + [InlineData("//symbol:Method[is-externally-visible('a')]")] + [InlineData("//symbol:Local[is-captured(.)]")] + [InlineData("//GotoStatement[file-path('a')]")] + public Task InvalidEntry_SemanticFunctionArgumentCount_Reported(string query) + { + var test = new AnalyzerTest { MarkupOptions = MarkupOptions.UseFirstDescriptor }; + test.TestState.AdditionalFiles.Add(("BannedSyntaxes.txt", "{|#0:" + query + "|}")); + test.TestCode = """ + class Sample + { + } + """; + test.ExpectedDiagnostics.Add(new DiagnosticResult("MA0241", DiagnosticSeverity.Warning).WithLocation(0)); + + return test.RunAsync(); + } }