Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 118 additions & 3 deletions docs/Rules/MA0240.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|-------|------------------|
Expand Down Expand Up @@ -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<T> 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<string>` 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<T>`, whatever `T` |
| `//symbol:Parameter[is-assignable-to('System.Collections.Generic.IEnumerable{System.String}')]` | The parameters of a type that implements `IEnumerable<string>` |
| `//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

Expand Down
4 changes: 3 additions & 1 deletion docs/Rules/MA0241.md
Original file line number Diff line number Diff line change
Expand Up @@ -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']`.
Expand Down
38 changes: 38 additions & 0 deletions src/Meziantou.Analyzer/Internals/AttributeDataElement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace Meziantou.Analyzer.Internals;

/// <summary>
/// An element of an <see cref="AttributeDataForest"/>: an attribute applied to a symbol, one of its arguments, or an
/// item of an argument that is an array.
/// </summary>
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";

/// <summary>The name of the element, such as <c>AttributeData</c> or <c>ConstructorArgument</c>.</summary>
public string Name { get; } = name;

/// <summary>The attribute the element is, or the attribute that contains the argument the element is.</summary>
public AttributeData Attribute { get; } = attribute;

/// <summary>The value of the argument or of the item. It is the default value for an attribute.</summary>
public TypedConstant Constant { get; } = constant;

/// <summary>The name of a named argument.</summary>
public string? ArgumentName { get; } = argumentName;

/// <summary>The position of a constructor argument or of an item. It is -1 for the other elements.</summary>
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; }
}
Loading
Loading