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
72 changes: 72 additions & 0 deletions docs/Rules/MA0240.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ Four prefixes expose a type. Each of them comes with the six suffixes described

| Suffix | Value |
|--------|-------|
| `…Name` | The name of the type, without its namespace, its containing types and its type arguments, such as `List` for `List<int>`, or `Int32` for `int`. It is not present for the types that have no name, such as an array or a pointer |
| `…MetadataName` | The metadata name of the type, described below |
| `…DocumentationId` | The documentation comment id of the type, described below |
| `…ReferenceId` | The reference id of the type, described below |
Expand All @@ -154,9 +155,11 @@ The other attributes are about the symbol itself and about the constants:
| Attribute | Value |
|-----------|-------|
| `semantic:Symbol` | The symbol the node refers to or declares, qualified by the metadata name of its containing type, such as `System.Console.WriteLine`. The parameters are not part of the name, so it selects all the overloads |
| `semantic:SymbolName` | The name of the symbol alone, without its containing type, such as `WriteLine`. It selects the members of this name whatever the type that declares them |
| `semantic:SymbolDocumentationId` | The documentation comment id of the symbol, such as `M:System.Console.WriteLine(System.String)`, which selects a single overload |
| `semantic:SymbolKind` | The kind of the symbol, such as `Method`, `Field`, `Property`, `NamedType`, or `Local` |
| `semantic:ContainingSymbol` | The symbol that contains the symbol, in the format of `semantic:Symbol`. It is the containing type of a member, the method of a local or of a parameter, and the containing namespace of a type that is not nested |
| `semantic:ContainingSymbolName` | The name of the containing symbol alone, such as `Execute` for a local declared in `Execute` |
| `semantic:ContainingSymbolDocumentationId` | The documentation comment id of the containing symbol, such as `M:Sample.Execute` for a local declared in `Execute` |
| `semantic:ContainingSymbolKind` | The kind of the containing symbol, such as `NamedType`, `Method`, or `Namespace` |
| `semantic:DeclaredAccessibility` | The declared accessibility of the symbol: `Private`, `ProtectedAndInternal` for `private protected`, `Protected`, `Internal`, `ProtectedOrInternal` for `protected internal`, or `Public`. It is not present for the symbols that have no accessibility, such as a local or a parameter |
Expand Down Expand Up @@ -198,6 +201,75 @@ The `semantic:ReturnType` attributes are the only ones about a declared type, an

An entry that uses the `semantic` prefix needs the semantic model, so it costs more than a syntactic query. The entries that do not use it are still evaluated without it, and a project whose files contain no `semantic` prefix never requests a semantic model.

### Operations

The compiler also builds an *operation tree*, which is what the code means rather than how it is written: an implicit conversion, a boxing, the method an invocation really calls, or the fact that a value is a compile-time constant are nodes and properties of this tree, whereas the syntax tree only has the text. The elements of the `operation` namespace expose it, so a query can select these constructs instead of approximating them:

````text
//operation:Conversion[@IsImplicit='true' and @TypeMetadataName='System.Object']; Do not box values
//operation:Invocation[@TargetMethodDocumentationId='M:System.Console.WriteLine(System.String)']; Use the logger
//operation:Loop//operation:Invocation[@TargetMethodName='Query']; Do not query in a loop
````

- Each operation is an element of the `operation` namespace named after its [`OperationKind`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.operationkind), such as `operation:Invocation`, `operation:Binary` or `operation:Conversion`. The children of an element are the child operations. A few kinds have an obsolete alias, such as `BinaryOperator` for `Binary`: the name of the element is always the current name, and [MA0241](MA0241.md) reports the alias.
- The operations of a file form a forest, not a single tree: the body of each member, the initializer of each field, of each property and of each parameter, and each attribute is a root. Only the executable code has operations, so the declarations themselves, such as a class or the parameter list of a method, are only in the syntax tree.
- An entry uses the syntax tree or the operations, never both: a query that uses the `operation` prefix is evaluated on the operations, and the `semantic` attributes are not available in it, as the operations expose their own. Write two entries to ban the two forms.
- A query that uses the `operation` prefix needs the semantic model, like an entry using the `semantic` attributes.

The diagnostic uses the qualified name of the element, so it is `The syntax 'operation:Conversion' is banned`.

#### Attributes

The attributes are named after the properties of the [operation interfaces](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.operations), and they are not prefixed. The kind is not an attribute, as it is the name of the element, and the properties that return the child operations are not attributes either, as they are the child elements.

| The property returns | The attributes of a property named `P` |
|----------------------|----------------------------------------|
| A type, such as `Type` or `TypeOperand` | `@PName`, `@PMetadataName`, `@PDocumentationId`, `@PReferenceId`, `@PIsValueType`, `@PNullableAnnotation` and `@PSpecialType`, with the values described in the previous section |
| Another symbol, such as `TargetMethod`, `Local` or `Parameter` | `@P` is the name qualified by the metadata name of its containing type, and `@PName`, `@PDocumentationId`, `@PKind` and `@PIsStatic` are the name alone, the documentation comment id, the kind of the symbol and whether it is static |
| Several symbols, such as `Locals` or `InitializedFields` | `@P` and `@PName`, the qualified names and the names alone, separated by a space |
| A boolean, such as `IsImplicit`, `IsVirtual` or `IsChecked` | `@P`, which is `true` or `false` |
| An enumeration, such as `OperatorKind` or `LoopKind` | `@P`, the name of the member, such as `Add` or `While` |
| A string or a number | `@P` |
| `ConstantValue` | `@HasConstantValue` and `@ConstantValue`, like the `semantic` attributes of the same name |

| Query | Reported operations |
|-------|---------------------|
| `//operation:Invocation[@TargetMethodName='Parse']` | The calls to a method named `Parse`, whatever the type that declares it |
| `//operation:Invocation[@IsVirtual='true']` | The virtual calls |
| `//operation:Binary[@OperatorKind='Equals']/*[@TypeSpecialType='System_String']` | The operands of the comparisons of strings with `==` |
| `//operation:Loop[@LoopKind='ForEach']` | The `foreach` loops |
| `//operation:Literal[@HasConstantValue='true' and @ConstantValue='0']` | The literals `0` |
| `//operation:PropertyReference[@PropertyDocumentationId='P:System.DateTime.Now']` | The uses of `DateTime.Now` |

The compiler inserts operations that have no syntax of their own, such as the conversion of an argument, the receiver of a call to an instance member of the same type, or the wrapper around each argument. Their `@IsImplicit` is `true`, and they are reported on the syntax of the construct that contains them, so `[@IsImplicit='false']` selects the operations the code writes explicitly.

The attributes an operation has depend on its kind, so `@TargetMethod` is only present on an invocation. An attribute that does not exist selects nothing instead of being reported, as for the attributes of the syntax nodes.

The text of an element is the source code of its operation, so `//operation:Invocation[contains(., '@"')]` selects the calls whose code contains a verbatim string. This is also true of the elements of the syntax tree.

#### The syntax of an operation

The `syntax` function returns the nodes of the syntax tree of the operations it is given, so a query on the operations can continue on the syntax tree. It is the way to reach what only the syntax has, such as whether a string is verbatim or interpolated, whereas the operations only have its value:

````text
syntax(//operation:Invocation[@TargetMethodName='Write'])//InterpolatedStringExpression; Do not interpolate the messages
//operation:Invocation[syntax(.)//InterpolatedStringExpression]; Do not interpolate the arguments
````

A function call cannot be a step of a path, so `//operation:Invocation/syntax()` is not a valid XPath query: the function takes the operations as its argument, and the path continues after the call.

| Query | Reported |
|-------|----------|
| `syntax(//operation:Invocation)` | The syntax nodes of the invocations, reported as `InvocationExpression` |
| `syntax(//operation:Invocation)//StringLiteralExpression/@Token[starts-with(., '@')]` | The verbatim strings used in a call |
| `//operation:Invocation[syntax(.)//InterpolatedStringExpression]` | The invocations whose syntax contains an interpolated string, reported as `operation:Invocation` |

- The nodes the function returns are reported with the name of their kind, like the nodes a query on the syntax tree selects, so a query can report an operation or a node depending on where it ends.
- `syntax(.)` in a predicate is the node of the operation the predicate is evaluated on.
- Several operations can share a syntax node, such as an expression and the implicit conversion that wraps it, and the node is only returned once.
- The nodes have their token attributes, but not the `semantic` attributes, as they are not available in a query on the operations. The operations expose the same data with their own attributes, such as `@TypeMetadataName`.
- The two documents are not merged, so the result of a union such as `//operation:Invocation | syntax(//operation:Binary)` is not in a defined order. Use two entries instead.

### 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, or an unknown severity, are reported by [MA0241](MA0241.md). The other lines of the file are still applied.
Expand Down
6 changes: 4 additions & 2 deletions docs/Rules/MA0241.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ 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.
- It is not a valid XPath 1.0 query, or it uses a function that does not exist. `syntax` is the only function this rule adds to the ones of XPath 1.0.
- The query does not return a node-set, such as `count(//ClassDeclaration)`.
- It uses a namespace prefix other than `semantic`, which is the only one that is defined.
- It uses a namespace prefix other than `semantic` and `operation`, 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']`.
- It uses a name that is not a kind of operation in the `operation` namespace, such as `//operation:Invokation`. The obsolete aliases of the kinds, such as `BinaryOperator` for `Binary`, are not valid either, as the elements are named after the current name.
- It uses both the `semantic` and the `operation` prefixes, such as `//operation:Invocation[@semantic:Symbol='a']`. A query is evaluated on the syntax tree or on the operations, and the operations expose their own attributes.
- Its severity field is not one of `none`, `silent`, `hidden`, `suggestion`, `info`, `warning` and `error`, such as `GotoStatement;warnign;Use a loop`.

The other lines of the file are still applied.
Expand Down
129 changes: 129 additions & 0 deletions src/Meziantou.Analyzer/Internals/BannedSyntaxXsltContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;

namespace Meziantou.Analyzer.Internals;

/// <summary>
/// The context of the XPath queries of the banned syntax files. It defines the <c>semantic</c> and <c>operation</c>
/// prefixes, and the <c>syntax</c> function, which returns the syntax nodes of the operations it is given, so a query
/// on the operations can continue on the syntax tree.
/// </summary>
internal sealed class BannedSyntaxXsltContext : XsltContext
{
/// <summary>The name of the function that returns the syntax nodes of a set of operations.</summary>
public const string SyntaxFunctionName = "syntax";

/// <summary>
/// A context whose <c>syntax</c> function returns nothing, which is used to validate a query without a compilation.
/// </summary>
public static readonly BannedSyntaxXsltContext Empty = new(syntaxNavigatorFactory: null);

private readonly Func<SyntaxNodeXPathNavigator>? _syntaxNavigatorFactory;

public BannedSyntaxXsltContext(Func<SyntaxNodeXPathNavigator>? syntaxNavigatorFactory)
: base(new NameTable())
{
// The context resolves the prefixes when the expression is evaluated, so it defines the same ones as the
// resolver that compiles it
AddNamespace(XPathNamespaces.SemanticPrefix, XPathNamespaces.SemanticNamespaceUri);
AddNamespace(XPathNamespaces.OperationPrefix, XPathNamespaces.OperationNamespaceUri);
_syntaxNavigatorFactory = syntaxNavigatorFactory;
}

public override bool Whitespace => false;

public override bool PreserveWhitespace(XPathNavigator node) => false;

public override int CompareDocument(string baseUri, string nextbaseUri) => string.CompareOrdinal(baseUri, nextbaseUri);

// The queries have no variable. The base type is not annotated, and null is how it says that a name is unknown.
public override IXsltContextVariable ResolveVariable(string prefix, string name) => null!;

// 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);

return null!;
}

private sealed class SyntaxFunction(Func<SyntaxNodeXPathNavigator>? syntaxNavigatorFactory) : IXsltContextFunction
{
public int Minargs => 1;

public int Maxargs => 1;

public XPathResultType ReturnType => XPathResultType.NodeSet;

public XPathResultType[] ArgTypes { get; } = [XPathResultType.NodeSet];

public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext)
{
if (syntaxNavigatorFactory is null || args is not [XPathNodeIterator operations])
return SyntaxNodeIterator.Empty;

// Several operations can share the same syntax node, such as an expression and the conversion that wraps it
var nodes = new List<SyntaxNode>();
var visited = new HashSet<SyntaxNode>();
while (operations.MoveNext())
{
if (operations.Current is OperationXPathNavigator { Operation: { } operation } && visited.Add(operation.Syntax))
{
nodes.Add(operation.Syntax);
}
}

return nodes.Count is 0 ? SyntaxNodeIterator.Empty : new SyntaxNodeIterator(syntaxNavigatorFactory(), nodes, index: -1);
}
}

private sealed class SyntaxNodeIterator : XPathNodeIterator
{
public static readonly SyntaxNodeIterator Empty = new(navigator: null, [], index: -1);

private readonly SyntaxNodeXPathNavigator? _navigator;
private readonly List<SyntaxNode> _nodes;
private SyntaxNodeXPathNavigator? _current;
private int _index;

public SyntaxNodeIterator(SyntaxNodeXPathNavigator? navigator, List<SyntaxNode> nodes, int index)
{
_navigator = navigator;
_nodes = nodes;
_index = index;
if (index >= 0 && index < nodes.Count)
{
_current = navigator!.CreateAt(nodes[index]);
}
}

// The engine reads the navigator of the current position, so it is the same instance that is moved
public override XPathNavigator? Current => _current;

public override int CurrentPosition => _index + 1;

public override int Count => _nodes.Count;

public override XPathNodeIterator Clone() => new SyntaxNodeIterator(_navigator, _nodes, _index);

public override bool MoveNext()
{
if (_index + 1 >= _nodes.Count)
return false;

_index++;
if (_current is null)
{
_current = _navigator!.CreateAt(_nodes[_index]);
}
else
{
_current.MoveToNode(_nodes[_index]);
}

return true;
}
}
}
18 changes: 18 additions & 0 deletions src/Meziantou.Analyzer/Internals/IBannedSyntaxNavigator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.CodeAnalysis.Text;

namespace Meziantou.Analyzer.Internals;

/// <summary>
/// The position of a navigator, as the banned syntax rule reports it.
/// </summary>
internal interface IBannedSyntaxNavigator
{
/// <summary>The span of the node or of the tokens of the attribute the navigator is positioned on.</summary>
TextSpan Span { get; }

/// <summary>
/// The name to report, such as <c>GotoStatement</c> or <c>operation:Invocation/@TargetMethod</c>. It is null
/// when the position is not reportable, such as the document that contains the elements.
/// </summary>
string? ReportName { get; }
}
Loading
Loading