Skip to content

Commit 33c0d5c

Browse files
Tweak diagnostic spans (#23)
1 parent 952e994 commit 33c0d5c

2 files changed

Lines changed: 213 additions & 94 deletions

File tree

src/CustomCode-Analyzer/Analyzer.cs

Lines changed: 115 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.CodeAnalysis;
22
using Microsoft.CodeAnalysis.CSharp.Syntax;
33
using Microsoft.CodeAnalysis.Diagnostics;
4+
using Microsoft.CodeAnalysis.Text;
45
using System;
56
using System.Collections.Concurrent;
67
using System.Collections.Generic;
@@ -134,7 +135,8 @@ public static class Categories
134135
customTags: WellKnownDiagnosticTags.CompilationEnd,
135136
helpLinkUri: "https://www.outsystems.com/tk/redirect?g=OS-ELG-MODL-05008");
136137

137-
// https://www.outsystems.com/tk/redirect?g=OS-ELG-MODL-05009 - not implementing
138+
// https://www.outsystems.com/tk/redirect?g=OS-ELG-MODL-05009 - not possible to implement
139+
// (see https://github.com/jonathanalgar/CustomCode-Analyzer/issues/11)
138140

139141
private static readonly DiagnosticDescriptor NonPublicStructRule = new(
140142
DiagnosticIds.NonPublicStruct,
@@ -544,14 +546,19 @@ Location GetMemberLocation()
544546
IPropertySymbol p => p.Type,
545547
_ => null
546548
};
547-
548549
// If the type is not valid, report the unsupported parameter type
549550
if (memberType != null && !IsValidParameterType(memberType, context.Compilation))
550551
{
551-
// Get the location of the member's type
552-
var loc = member.DeclaringSyntaxReferences.First().GetSyntax().GetLocation();
552+
// Get the location based on whether it's a field or property
553+
Location loc = member switch
554+
{
555+
IFieldSymbol _ when member.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax()
556+
is VariableDeclaratorSyntax fieldSyntax => fieldSyntax.Identifier.GetLocation(),
557+
IPropertySymbol _ when member.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax()
558+
is PropertyDeclarationSyntax propSyntax => propSyntax.Identifier.GetLocation(),
559+
_ => member.DeclaringSyntaxReferences.First().GetSyntax().GetLocation()
560+
};
553561

554-
// Report diagnostic if the parameter type is unsupported
555562
context.ReportDiagnostic(
556563
Diagnostic.Create(
557564
UnsupportedParameterTypeRule,
@@ -594,11 +601,22 @@ private static void AnalyzeInterface(
594601

595602
// Extract library name from attribute or interface name
596603
string libraryName = null;
604+
Location nameLocation = null;
605+
597606
// First, check the 'Name' property
598607
var nameArg = osInterfaceAttr.NamedArguments.FirstOrDefault(na => na.Key == "Name");
599608
if (nameArg.Key != null && nameArg.Value.Value is string specifiedName)
600609
{
601610
libraryName = specifiedName;
611+
// Get the location from the attribute argument syntax
612+
var attrSyntax = osInterfaceAttr.ApplicationSyntaxReference?.GetSyntax() as AttributeSyntax;
613+
var nameArgSyntax = attrSyntax?.ArgumentList?.Arguments
614+
.FirstOrDefault(arg => arg.NameEquals?.Name.Identifier.ValueText == "Name")
615+
?.Expression as LiteralExpressionSyntax;
616+
if (nameArgSyntax != null)
617+
{
618+
nameLocation = nameArgSyntax.GetLocation();
619+
}
602620
}
603621
else
604622
{
@@ -607,12 +625,26 @@ private static void AnalyzeInterface(
607625
if (originalNameArg.Key != null && originalNameArg.Value.Value is string originalName)
608626
{
609627
libraryName = originalName;
628+
// Get the location from the attribute argument syntax
629+
var attrSyntax = osInterfaceAttr.ApplicationSyntaxReference?.GetSyntax() as AttributeSyntax;
630+
var originalNameArgSyntax = attrSyntax?.ArgumentList?.Arguments
631+
.FirstOrDefault(arg => arg.NameEquals?.Name.Identifier.ValueText == "OriginalName")
632+
?.Expression as LiteralExpressionSyntax;
633+
if (originalNameArgSyntax != null)
634+
{
635+
nameLocation = originalNameArgSyntax.GetLocation();
636+
}
610637
}
611638
}
639+
612640
// If no name specified in attributes, use the interface name without 'I' prefix
613-
libraryName ??= (typeSymbol.Name.StartsWith("I", StringComparison.Ordinal))
614-
? typeSymbol.Name.Substring(1)
615-
: typeSymbol.Name;
641+
if (libraryName == null)
642+
{
643+
libraryName = (typeSymbol.Name.StartsWith("I", StringComparison.Ordinal))
644+
? typeSymbol.Name.Substring(1)
645+
: typeSymbol.Name;
646+
nameLocation = syntax.Identifier.GetLocation();
647+
}
616648

617649
// Validate name constraints
618650
if (libraryName.Length > 50)
@@ -621,7 +653,7 @@ private static void AnalyzeInterface(
621653
context.ReportDiagnostic(
622654
Diagnostic.Create(
623655
NameMaxLengthExceededRule,
624-
syntax.GetLocation(),
656+
nameLocation ?? syntax.GetLocation(), // Fall back to full syntax if we couldn't get specific location
625657
libraryName));
626658
}
627659

@@ -631,7 +663,7 @@ private static void AnalyzeInterface(
631663
context.ReportDiagnostic(
632664
Diagnostic.Create(
633665
NameBeginsWithNumbersRule,
634-
syntax.GetLocation(),
666+
nameLocation ?? syntax.GetLocation(),
635667
libraryName));
636668
}
637669

@@ -642,7 +674,7 @@ private static void AnalyzeInterface(
642674
context.ReportDiagnostic(
643675
Diagnostic.Create(
644676
UnsupportedCharactersInNameRule,
645-
syntax.GetLocation(),
677+
nameLocation ?? syntax.GetLocation(),
646678
libraryName,
647679
string.Join(", ", invalidChars)));
648680
}
@@ -653,7 +685,12 @@ private static void AnalyzeInterface(
653685
context.ReportDiagnostic(
654686
Diagnostic.Create(
655687
NonPublicInterfaceRule,
656-
syntax.GetLocation(),
688+
Location.Create(
689+
syntax.SyntaxTree,
690+
TextSpan.FromBounds(
691+
syntax.Keyword.SpanStart,
692+
syntax.Identifier.Span.End
693+
)),
657694
typeSymbol.Name));
658695
}
659696
}
@@ -682,12 +719,17 @@ private static void AnalyzeMethod(SymbolAnalysisContext context, IMethodSymbol m
682719
if ((hasOSInterfaceAttribute || implementsOSInterface) &&
683720
methodSymbol.Name.StartsWith("_", StringComparison.Ordinal))
684721
{
685-
context.ReportDiagnostic(
686-
Diagnostic.Create(
687-
NameBeginsWithUnderscoresRule,
688-
methodSyntax.GetLocation(),
689-
"Method",
690-
methodSymbol.Name));
722+
if (syntaxRef.GetSyntax() is MethodDeclarationSyntax methodDeclSyntax)
723+
{
724+
var identifierLocation = methodDeclSyntax.Identifier.GetLocation();
725+
726+
context.ReportDiagnostic(
727+
Diagnostic.Create(
728+
NameBeginsWithUnderscoresRule,
729+
identifierLocation,
730+
"Method",
731+
methodSymbol.Name));
732+
}
691733
}
692734

693735
// Only check for by-ref parameters in the OSInterface itself (not the implementation)
@@ -785,11 +827,32 @@ private static void AnalyzeClass(SymbolAnalysisContext context, INamedTypeSymbol
785827
if (!typeSymbol.DeclaredAccessibility.HasFlag(Accessibility.Public) &&
786828
typeSymbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is ClassDeclarationSyntax clsDecl)
787829
{
788-
context.ReportDiagnostic(
789-
Diagnostic.Create(
790-
MissingPublicImplementationRule,
791-
clsDecl.Identifier.GetLocation(),
792-
implementedInterface.Name));
830+
if (clsDecl.Modifiers.Any())
831+
{
832+
// Start from first modifier (internal) and go through the class name
833+
context.ReportDiagnostic(
834+
Diagnostic.Create(
835+
MissingPublicImplementationRule,
836+
Location.Create(
837+
clsDecl.SyntaxTree,
838+
TextSpan.FromBounds(
839+
clsDecl.Modifiers.First().SpanStart,
840+
clsDecl.Identifier.Span.End)),
841+
implementedInterface.Name));
842+
}
843+
else
844+
{
845+
// If no modifiers, start from class keyword through the class name
846+
context.ReportDiagnostic(
847+
Diagnostic.Create(
848+
MissingPublicImplementationRule,
849+
Location.Create(
850+
clsDecl.SyntaxTree,
851+
TextSpan.FromBounds(
852+
clsDecl.Keyword.SpanStart,
853+
clsDecl.Identifier.Span.End)),
854+
implementedInterface.Name));
855+
}
793856
}
794857

795858
// Must have a public parameterless constructor
@@ -855,7 +918,7 @@ private static void AnalyzeCompilationEnd(
855918
context.ReportDiagnostic(
856919
Diagnostic.Create(
857920
NoSingleInterfaceRule,
858-
ifDecl.GetLocation()));
921+
ifDecl.Identifier.GetLocation()));
859922
}
860923
}
861924
else if (osInterfaces.Count > 1)
@@ -869,8 +932,14 @@ private static void AnalyzeCompilationEnd(
869932
// Create a comma-separated list of interface names
870933
var interfaceNames = string.Join(", ", osInterfaces.Keys.OrderBy(n => n));
871934
// Report diagnostic indicating multiple OSInterfaces
872-
context.ReportDiagnostic(
873-
Diagnostic.Create(ManyInterfacesRule, firstSyntax.GetLocation(), interfaceNames));
935+
foreach (var osInterface in osInterfaces.Values)
936+
{
937+
context.ReportDiagnostic(
938+
Diagnostic.Create(
939+
ManyInterfacesRule,
940+
osInterface.Syntax.Identifier.GetLocation(),
941+
interfaceNames));
942+
}
874943
}
875944
else
876945
{
@@ -890,7 +959,7 @@ private static void AnalyzeCompilationEnd(
890959
context.ReportDiagnostic(
891960
Diagnostic.Create(
892961
MissingImplementationRule,
893-
syntax.GetLocation(),
962+
syntax.Identifier.GetLocation(),
894963
symbol.Name));
895964
}
896965
else if (implementations.Count > 1)
@@ -899,13 +968,16 @@ private static void AnalyzeCompilationEnd(
899968
var implNames = string.Join(", ",
900969
implementations.Select(i => i.Name).OrderBy(n => n));
901970

902-
// Report diagnostic indicating multiple implementations
903-
context.ReportDiagnostic(
904-
Diagnostic.Create(
905-
ManyImplementationRule,
906-
syntax.GetLocation(),
907-
symbol.Name,
908-
implNames));
971+
// Report diagnostics indicating multiple implementations
972+
foreach (var implementingClass in implementations)
973+
{
974+
context.ReportDiagnostic(
975+
Diagnostic.Create(
976+
ManyImplementationRule,
977+
implementingClass.Locations.First(), // Location of each implementing class
978+
symbol.Name,
979+
implNames));
980+
}
909981
}
910982
}
911983

@@ -920,18 +992,15 @@ private static void AnalyzeCompilationEnd(
920992

921993
foreach (var duplicate in duplicates)
922994
{
923-
// Get the first struct (ordered by name) for consistent error reporting
924-
var firstStruct = duplicate.OrderBy(d => d.Name).First();
925-
926-
// Create a comma-separated list of struct names that share the same name
927-
var structNames = string.Join(", ", duplicate.Select(d => d.Name).OrderBy(n => n));
928-
929-
context.ReportDiagnostic(
930-
Diagnostic.Create(
931-
DuplicateNameRule,
932-
firstStruct.Locations.First(),
933-
structNames,
934-
duplicate.Key));
995+
// Report diagnostic for each duplicate instance
996+
foreach (var struct_ in duplicate)
997+
{
998+
context.ReportDiagnostic(
999+
Diagnostic.Create(
1000+
DuplicateNameRule,
1001+
struct_.Locations.First(),
1002+
duplicate.Key));
1003+
}
9351004
}
9361005
}
9371006

0 commit comments

Comments
 (0)