From 819d4506f9b2127d3486e60362f8c2c761fe68b0 Mon Sep 17 00:00:00 2001 From: Vlad Catalina Date: Wed, 26 Nov 2025 17:09:02 +0200 Subject: [PATCH 1/2] REVIT-244971 * add analyzer to detect usage of nodes (functions) marked with NodeObsolete from Dynamo Core. --- src/DynamoRevit.All.sln | 10 + .../AnalyzerReleases.Shipped.md | 2 + .../AnalyzerReleases.Unshipped.md | 8 + .../NodeObsoleteAnalyzer.cs | 276 ++++++++++++++++++ src/Libraries/RevitNodes.Analyzers/README.md | 62 ++++ .../RevitNodes.Analyzers.csproj | 17 ++ src/Libraries/RevitNodes/RevitNodes.csproj | 3 + .../RevitNodesUI/RevitNodesUI.csproj | 3 + 8 files changed, 381 insertions(+) create mode 100644 src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Shipped.md create mode 100644 src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Unshipped.md create mode 100644 src/Libraries/RevitNodes.Analyzers/NodeObsoleteAnalyzer.cs create mode 100644 src/Libraries/RevitNodes.Analyzers/README.md create mode 100644 src/Libraries/RevitNodes.Analyzers/RevitNodes.Analyzers.csproj diff --git a/src/DynamoRevit.All.sln b/src/DynamoRevit.All.sln index 70bd05e904..813d2541ec 100644 --- a/src/DynamoRevit.All.sln +++ b/src/DynamoRevit.All.sln @@ -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}" @@ -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 diff --git a/src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Shipped.md b/src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000000..0dd62e3147 --- /dev/null +++ b/src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,2 @@ +## Release 1.0 +### New Rules diff --git a/src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Unshipped.md b/src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000000..a072149254 --- /dev/null +++ b/src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Unshipped.md @@ -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 + diff --git a/src/Libraries/RevitNodes.Analyzers/NodeObsoleteAnalyzer.cs b/src/Libraries/RevitNodes.Analyzers/NodeObsoleteAnalyzer.cs new file mode 100644 index 0000000000..03e2700e19 --- /dev/null +++ b/src/Libraries/RevitNodes.Analyzers/NodeObsoleteAnalyzer.cs @@ -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 +{ + /// + /// Analyzer that detects usage of types, methods, or properties marked with [NodeObsolete] attribute + /// from DynamoVisualProgramming dependencies and reports compiler warnings. + /// + [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 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; + } + } +} + diff --git a/src/Libraries/RevitNodes.Analyzers/README.md b/src/Libraries/RevitNodes.Analyzers/README.md new file mode 100644 index 0000000000..43ed140731 --- /dev/null +++ b/src/Libraries/RevitNodes.Analyzers/README.md @@ -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) + diff --git a/src/Libraries/RevitNodes.Analyzers/RevitNodes.Analyzers.csproj b/src/Libraries/RevitNodes.Analyzers/RevitNodes.Analyzers.csproj new file mode 100644 index 0000000000..b812c7e662 --- /dev/null +++ b/src/Libraries/RevitNodes.Analyzers/RevitNodes.Analyzers.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + false + false + latest + true + + + + + + + + + diff --git a/src/Libraries/RevitNodes/RevitNodes.csproj b/src/Libraries/RevitNodes/RevitNodes.csproj index 4d9940c1cf..394a48481c 100644 --- a/src/Libraries/RevitNodes/RevitNodes.csproj +++ b/src/Libraries/RevitNodes/RevitNodes.csproj @@ -82,4 +82,7 @@ + + + \ No newline at end of file diff --git a/src/Libraries/RevitNodesUI/RevitNodesUI.csproj b/src/Libraries/RevitNodesUI/RevitNodesUI.csproj index e4a62734e3..2b0b5fe22c 100644 --- a/src/Libraries/RevitNodesUI/RevitNodesUI.csproj +++ b/src/Libraries/RevitNodesUI/RevitNodesUI.csproj @@ -77,4 +77,7 @@ + + + \ No newline at end of file From 143f2ddf7c4faf05db00377323c25553114dbd3b Mon Sep 17 00:00:00 2001 From: Vlad Catalina Date: Wed, 26 Nov 2025 17:09:02 +0200 Subject: [PATCH 2/2] REVIT-244971 * add analyzer to detect usage of nodes (functions) marked with NodeObsolete from Dynamo Core. --- src/DynamoRevit.All.sln | 11 + .../AnalyzerReleases.Shipped.md | 2 + .../AnalyzerReleases.Unshipped.md | 8 + .../NodeObsoleteAnalyzer.cs | 276 ++++++++++++++++++ src/Libraries/RevitNodes.Analyzers/README.md | 62 ++++ .../RevitNodes.Analyzers.csproj | 17 ++ src/Libraries/RevitNodes/RevitNodes.csproj | 3 + .../RevitNodesUI/RevitNodesUI.csproj | 3 + 8 files changed, 382 insertions(+) create mode 100644 src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Shipped.md create mode 100644 src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Unshipped.md create mode 100644 src/Libraries/RevitNodes.Analyzers/NodeObsoleteAnalyzer.cs create mode 100644 src/Libraries/RevitNodes.Analyzers/README.md create mode 100644 src/Libraries/RevitNodes.Analyzers/RevitNodes.Analyzers.csproj diff --git a/src/DynamoRevit.All.sln b/src/DynamoRevit.All.sln index 70bd05e904..6c6ac4312c 100644 --- a/src/DynamoRevit.All.sln +++ b/src/DynamoRevit.All.sln @@ -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}" @@ -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 @@ -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} diff --git a/src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Shipped.md b/src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000000..0dd62e3147 --- /dev/null +++ b/src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,2 @@ +## Release 1.0 +### New Rules diff --git a/src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Unshipped.md b/src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000000..a072149254 --- /dev/null +++ b/src/Libraries/RevitNodes.Analyzers/AnalyzerReleases.Unshipped.md @@ -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 + diff --git a/src/Libraries/RevitNodes.Analyzers/NodeObsoleteAnalyzer.cs b/src/Libraries/RevitNodes.Analyzers/NodeObsoleteAnalyzer.cs new file mode 100644 index 0000000000..03e2700e19 --- /dev/null +++ b/src/Libraries/RevitNodes.Analyzers/NodeObsoleteAnalyzer.cs @@ -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 +{ + /// + /// Analyzer that detects usage of types, methods, or properties marked with [NodeObsolete] attribute + /// from DynamoVisualProgramming dependencies and reports compiler warnings. + /// + [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 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; + } + } +} + diff --git a/src/Libraries/RevitNodes.Analyzers/README.md b/src/Libraries/RevitNodes.Analyzers/README.md new file mode 100644 index 0000000000..43ed140731 --- /dev/null +++ b/src/Libraries/RevitNodes.Analyzers/README.md @@ -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) + diff --git a/src/Libraries/RevitNodes.Analyzers/RevitNodes.Analyzers.csproj b/src/Libraries/RevitNodes.Analyzers/RevitNodes.Analyzers.csproj new file mode 100644 index 0000000000..b812c7e662 --- /dev/null +++ b/src/Libraries/RevitNodes.Analyzers/RevitNodes.Analyzers.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + false + false + latest + true + + + + + + + + + diff --git a/src/Libraries/RevitNodes/RevitNodes.csproj b/src/Libraries/RevitNodes/RevitNodes.csproj index 4d9940c1cf..394a48481c 100644 --- a/src/Libraries/RevitNodes/RevitNodes.csproj +++ b/src/Libraries/RevitNodes/RevitNodes.csproj @@ -82,4 +82,7 @@ + + + \ No newline at end of file diff --git a/src/Libraries/RevitNodesUI/RevitNodesUI.csproj b/src/Libraries/RevitNodesUI/RevitNodesUI.csproj index e4a62734e3..2b0b5fe22c 100644 --- a/src/Libraries/RevitNodesUI/RevitNodesUI.csproj +++ b/src/Libraries/RevitNodesUI/RevitNodesUI.csproj @@ -77,4 +77,7 @@ + + + \ No newline at end of file