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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/DynamoRevit.All.sln
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RevitNodes", "Libraries\Rev
{133FC760-5699-46D9-BEA6-E816B5F01016} = {133FC760-5699-46D9-BEA6-E816B5F01016}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RevitNodes.Analyzers", "Libraries\RevitNodes.Analyzers\RevitNodes.Analyzers.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RevitNodesUI", "Libraries\RevitNodesUI\RevitNodesUI.csproj", "{75940ACC-3708-4526-8D91-7E3365BAF682}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RevitServices", "Libraries\RevitServices\RevitServices.csproj", "{E4701F9E-41AB-4044-8166-85D924FEB632}"
Expand Down Expand Up @@ -134,6 +136,14 @@ Global
{0BC2A611-BD0E-4FCC-A1DE-81F14ED369B2}.Release|NET100.Build.0 = Release|NET100
{0BC2A611-BD0E-4FCC-A1DE-81F14ED369B2}.Release|NET80.ActiveCfg = Release|NET80
{0BC2A611-BD0E-4FCC-A1DE-81F14ED369B2}.Release|NET80.Build.0 = Release|NET80
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|NET100.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|NET100.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|NET80.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|NET80.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|NET100.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|NET100.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|NET80.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|NET80.Build.0 = Release|Any CPU
{75940ACC-3708-4526-8D91-7E3365BAF682}.Debug|NET100.ActiveCfg = Debug|NET100
{75940ACC-3708-4526-8D91-7E3365BAF682}.Debug|NET100.Build.0 = Debug|NET100
{75940ACC-3708-4526-8D91-7E3365BAF682}.Debug|NET80.ActiveCfg = Debug|NET80
Expand Down Expand Up @@ -228,6 +238,7 @@ Global
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0BC2A611-BD0E-4FCC-A1DE-81F14ED369B2} = {FA7BE306-A3B0-45FA-9D87-0C69E6932C13}
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {FA7BE306-A3B0-45FA-9D87-0C69E6932C13}
{75940ACC-3708-4526-8D91-7E3365BAF682} = {FA7BE306-A3B0-45FA-9D87-0C69E6932C13}
{E4701F9E-41AB-4044-8166-85D924FEB632} = {FA7BE306-A3B0-45FA-9D87-0C69E6932C13}
{0E492D35-2310-4849-9694-A2A53C09F21B} = {F4D44BC0-32CF-4E58-AD2A-F19CE1450B00}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
## Release 1.0
### New Rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
DN001 | Usage | Warning | Reports when code uses a type, method, or property marked with [NodeObsolete] attribute
DN002 | Usage | Error | Reports when code uses a type, method, or property marked with [NodeObsolete(IsError = true)] attribute

276 changes: 276 additions & 0 deletions src/Libraries/RevitNodes.Analyzers/NodeObsoleteAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace RevitNodes.Analyzers
{
/// <summary>
/// Analyzer that detects usage of types, methods, or properties marked with [NodeObsolete] attribute
/// from DynamoVisualProgramming dependencies and reports compiler warnings.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class NodeObsoleteAnalyzer : DiagnosticAnalyzer
{
// Diagnostic IDs
public const string DiagnosticId = "DN001";
public const string ErrorDiagnosticId = "DN002";

// Diagnostic categories
private const string Category = "Usage";

// Diagnostic descriptors
private static readonly DiagnosticDescriptor WarningRule = new DiagnosticDescriptor(
DiagnosticId,
"Node is obsolete",
"{0}",
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "The node marked with [NodeObsolete] should not be used.");

private static readonly DiagnosticDescriptor ErrorRule = new DiagnosticDescriptor(
ErrorDiagnosticId,
"Node is obsolete (error)",
"{0}",
Category,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "The node marked with [NodeObsolete] should not be used.");

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(WarningRule, ErrorRule);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.IdentifierName);
context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression);
}

private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
var identifierName = (IdentifierNameSyntax)context.Node;

// Get the symbol for the identifier
var symbolInfo = context.SemanticModel.GetSymbolInfo(identifierName, context.CancellationToken);
if (symbolInfo.Symbol == null)
return;

CheckAndReportObsolete(symbolInfo.Symbol, identifierName.GetLocation(), context);
}

private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
{
var invocation = (InvocationExpressionSyntax)context.Node;
var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken);

if (symbolInfo.Symbol != null)
{
CheckAndReportObsolete(symbolInfo.Symbol, invocation.GetLocation(), context);
}
}

private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
{
var memberAccess = (MemberAccessExpressionSyntax)context.Node;
var symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccess, context.CancellationToken);

if (symbolInfo.Symbol != null)
{
CheckAndReportObsolete(symbolInfo.Symbol, memberAccess.Name.GetLocation(), context);
}
}

private static void CheckAndReportObsolete(ISymbol symbol, Location location, SyntaxNodeAnalysisContext context)
{
var nodeObsoleteAttribute = GetNodeObsoleteAttribute(symbol, context.Compilation);
if (nodeObsoleteAttribute == null)
return;

var message = GetObsoleteMessage(nodeObsoleteAttribute);
var isError = GetIsError(nodeObsoleteAttribute);
var symbolName = GetSymbolDisplayName(symbol);

var diagnosticMessage = string.IsNullOrEmpty(message)
? $"{symbolName} is obsolete."
: $"{symbolName} is obsolete: {message}";

var rule = isError ? ErrorRule : WarningRule;
var diagnostic = Diagnostic.Create(
rule,
location,
diagnosticMessage);

context.ReportDiagnostic(diagnostic);
}

private static INamedTypeSymbol GetNodeObsoleteAttributeType(Compilation compilation)
{
// Try multiple possible namespaces where NodeObsolete might be defined
var possibleNames = new[]
{
"Autodesk.DesignScript.Runtime.NodeObsoleteAttribute",
"Autodesk.DesignScript.Runtime.NodeObsolete",
"Dynamo.Graph.Nodes.NodeObsoleteAttribute",
"Dynamo.Graph.Nodes.NodeObsolete",
"NodeObsoleteAttribute",
"NodeObsolete"
};

foreach (var name in possibleNames)
{
var type = compilation.GetTypeByMetadataName(name);
if (type != null)
return type;
}

return null;
}

private static AttributeData GetNodeObsoleteAttribute(ISymbol symbol, Compilation compilation)
{
var nodeObsoleteAttributeType = GetNodeObsoleteAttributeType(compilation);
if (nodeObsoleteAttributeType == null)
return null;

// Check the symbol itself
foreach (var attribute in symbol.GetAttributes())
{
if (IsNodeObsoleteAttribute(attribute, nodeObsoleteAttributeType))
{
return attribute;
}
}

// Check the containing type if symbol is a method or property
if (symbol.ContainingType != null)
{
foreach (var attribute in symbol.ContainingType.GetAttributes())
{
if (IsNodeObsoleteAttribute(attribute, nodeObsoleteAttributeType))
{
return attribute;
}
}
}

// Check base types/interfaces for inherited attributes
if (symbol.ContainingType != null)
{
var baseType = symbol.ContainingType.BaseType;
while (baseType != null)
{
foreach (var attribute in baseType.GetAttributes())
{
if (IsNodeObsoleteAttribute(attribute, nodeObsoleteAttributeType))
{
return attribute;
}
}
baseType = baseType.BaseType;
}
}

return null;
}

private static bool IsNodeObsoleteAttribute(AttributeData attribute, INamedTypeSymbol expectedType)
{
if (attribute.AttributeClass == null)
return false;

// Direct type match
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, expectedType))
return true;

// Name match (handles cases where the type might be from a different assembly)
if (attribute.AttributeClass.Name == "NodeObsoleteAttribute" ||
attribute.AttributeClass.Name == "NodeObsolete")
return true;

// Check if it derives from or implements the expected type
var currentType = attribute.AttributeClass;
while (currentType != null)
{
if (SymbolEqualityComparer.Default.Equals(currentType, expectedType))
return true;
currentType = currentType.BaseType;
}

return false;
}

private static string GetObsoleteMessage(AttributeData attribute)
{
// Check constructor arguments first
if (attribute.ConstructorArguments.Length > 0)
{
var arg = attribute.ConstructorArguments[0];
if (arg.Kind == TypedConstantKind.Primitive && arg.Value is string message)
{
return message;
}
}

// Check named arguments for Message property
foreach (var namedArg in attribute.NamedArguments)
{
if (namedArg.Key == "Message" && namedArg.Value.Kind == TypedConstantKind.Primitive)
{
if (namedArg.Value.Value is string message)
{
return message;
}
}
}

return string.Empty;
}

private static bool GetIsError(AttributeData attribute)
{
// Check named arguments for IsError property
foreach (var namedArg in attribute.NamedArguments)
{
if (namedArg.Key == "IsError" && namedArg.Value.Kind == TypedConstantKind.Primitive)
{
if (namedArg.Value.Value is bool isError)
{
return isError;
}
}
}

return false;
}

private static string GetSymbolDisplayName(ISymbol symbol)
{
// Use the symbol's display format for better readability
if (symbol is IMethodSymbol method)
{
return $"{method.ContainingType?.Name ?? ""}.{method.Name}";
}
if (symbol is IPropertySymbol property)
{
return $"{property.ContainingType?.Name ?? ""}.{property.Name}";
}
if (symbol is IFieldSymbol field)
{
return $"{field.ContainingType?.Name ?? ""}.{field.Name}";
}
if (symbol is INamedTypeSymbol type)
{
return type.Name;
}

return symbol.Name;
}
}
}

62 changes: 62 additions & 0 deletions src/Libraries/RevitNodes.Analyzers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# NodeObsolete Analyzer

This Roslyn analyzer detects compile-time usage of types, methods, or properties marked with the `[NodeObsolete]` attribute from DynamoVisualProgramming dependencies.

## Purpose

When DynamoVisualProgramming marks APIs as obsolete using the `[NodeObsolete]` attribute, this analyzer ensures that your codebase gets compiler warnings (or errors) when you use those obsolete APIs, helping you migrate to newer alternatives before they're removed.

## How It Works

The analyzer:
1. Scans your code during compilation
2. Detects when you use symbols (classes, methods, properties) that have the `[NodeObsolete]` attribute
3. The attribute can be defined in external assemblies (like DynamoVisualProgramming packages)
4. Reports warnings or errors at the usage location

## Diagnostic IDs

- **DN001**: Warning when using a node/method/property marked with `[NodeObsolete]`
- **DN002**: Error when using a node/method/property marked with `[NodeObsolete(IsError = true)]`

## Integration

The analyzer is automatically included when you build the `RevitNodes` project. It will analyze all code that references DynamoVisualProgramming assemblies.

## Example

If DynamoVisualProgramming has:

```csharp
[NodeObsolete("Use NewMethod instead")]
public static void OldMethod() { }
```

And your code uses it:

```csharp
OldMethod(); // This will generate a compiler warning: "OldMethod is obsolete: Use NewMethod instead"
```

## Supported Usage Patterns

The analyzer detects:
- Direct type references: `var x = new ObsoleteType();`
- Method invocations: `ObsoleteMethod();`
- Property access: `var x = ObsoleteProperty;`
- Member access: `obj.ObsoleteMember`

## Attribute Detection

The analyzer searches for `NodeObsolete` attributes in these namespaces:
- `Autodesk.DesignScript.Runtime.NodeObsoleteAttribute`
- `Autodesk.DesignScript.Runtime.NodeObsolete`
- `Dynamo.Graph.Nodes.NodeObsoleteAttribute`
- `Dynamo.Graph.Nodes.NodeObsolete`
- And other common locations

It also checks:
- Attributes on the symbol itself
- Attributes on containing types (for methods/properties)
- Attributes on base types (inherited attributes)

Loading