diff --git a/.editorconfig b/.editorconfig index 1d0da3b9..05778cd0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ [*.cs] -# DAP000: Interceptors generated -dotnet_diagnostic.DAP000.severity = suggestion +# RS2008: Enable analyzer release tracking +dotnet_diagnostic.RS2008.severity = none diff --git a/Directory.Build.props b/Directory.Build.props index 6cfe30d0..db609fb0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -26,6 +26,7 @@ en-US false latest-Recommended + ($Features);strict diff --git a/docs/docs.csproj b/docs/docs.csproj index 33e77764..0c37a108 100644 --- a/docs/docs.csproj +++ b/docs/docs.csproj @@ -5,4 +5,7 @@ false false + + + diff --git a/docs/rules/DAP003.md b/docs/rules/DAP003.md new file mode 100644 index 00000000..70c75e63 --- /dev/null +++ b/docs/rules/DAP003.md @@ -0,0 +1,12 @@ +# DAP003 + +Interceptors are a new feature, and require some project configuration. + +The exact shape of this has changed repeatedly, so this guidance is not complete yet. + +Things that might be involved, depending on what preview build SDK you're using: + +- `($Features);InterceptorsPreview` +- `$(InterceptorsPreviewNamespaces);Dapper.AOT` + +This will be clarified for release. \ No newline at end of file diff --git a/docs/rules/DAP004.md b/docs/rules/DAP004.md new file mode 100644 index 00000000..d00f9d1b --- /dev/null +++ b/docs/rules/DAP004.md @@ -0,0 +1,10 @@ +# DAP004 + +Dapper.AOT runs on interceptors, which requires at least C# 11. The language version is implied by +your target .NET version, but can be changed manually - it is fine to use a different language +version than the default for that target runtime, especially when going "higher". For example, you +can use C# 12 on a project that targets .NET Framework (as long as you have up-to-date build tools). + +The easiest way to do this is via `latest`. + +For more information, see https://learn.microsoft.com/dotnet/csharp/language-reference/configure-language-version \ No newline at end of file diff --git a/docs/rules/DAP005.md b/docs/rules/DAP005.md new file mode 100644 index 00000000..269f0af0 --- /dev/null +++ b/docs/rules/DAP005.md @@ -0,0 +1,22 @@ +# DAP005 + +You're seeing this message because Dapper.AOT has found at least one place where it *could* help, and it +isn't explicit whether you want it to do so. + +Dapper.AOT doesn't change any behaviours without your permission. You can *enable* (or disable) Dapper.AOT +at any level by adding a `[DapperAot]` (or `[DapperAot(false)]`) attribute. + +To enable Dapper.AOT globally, add (usually to `AssemblyInfo.cs`, although it doesn't matter where): + + +``` csharp +[module: DapperAot] +``` + +Alternatively, if you only want it in a few places, use: + +``` csharp +[module: DapperAot(false)] +``` + +and add `[DapperAot]` at more specific levels - types, individual methods, etc. \ No newline at end of file diff --git a/src/Dapper.AOT.Analyzers/AnalyzerReleases.Shipped.md b/src/Dapper.AOT.Analyzers/AnalyzerReleases.Shipped.md deleted file mode 100644 index a898a733..00000000 --- a/src/Dapper.AOT.Analyzers/AnalyzerReleases.Shipped.md +++ /dev/null @@ -1,72 +0,0 @@ -; Shipped analyzer releases -; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md - -Rule ID | Category | Severity | Notes ---------|----------|----------|------- -DAP000 | Library | Hidden | Diagnostics -DAP001 | Library | Info | Diagnostics -; DAP002 | Library | Info | Diagnostics -DAP003 | Library | Warning | Diagnostics -DAP004 | Library | Warning | Diagnostics -DAP005 | Library | Info | Diagnostics -DAP006 | Library | Warning | Diagnostics -DAP007 | Library | Info | Diagnostics -; DAP008 | Library | Info | Diagnostics -DAP009 | Library | Info | Diagnostics -; DAP010 | Library | Info | Diagnostics -DAP011 | Library | Warning | Diagnostics -DAP012 | Library | Warning | Diagnostics -DAP013 | Library | Info | Diagnostics -DAP014 | Library | Info | Diagnostics -DAP015 | Library | Info | Diagnostics -DAP016 | Library | Info | Diagnostics -DAP017 | Library | Info | Diagnostics -DAP018 | Sql | Warning | Diagnostics -DAP019 | Sql | Error | Diagnostics -DAP020 | Sql | Error | Diagnostics -DAP021 | Sql | Warning | Diagnostics -DAP022 | Sql | Warning | Diagnostics -DAP023 | Sql | Warning | Diagnostics -DAP024 | Sql | Warning | Diagnostics -DAP025 | Sql | Warning | Diagnostics -DAP026 | Sql | Error | Diagnostics -DAP027 | Performance | Warning | Diagnostics -DAP028 | Performance | Warning | Diagnostics -DAP029 | Library | Info | Diagnostics -DAP030 | Library | Error | Diagnostics -DAP031 | Library | Error | Diagnostics -DAP032 | Library | Error | Diagnostics -DAP033 | Library | Warning | Diagnostics -DAP034 | Library | Warning | Diagnostics -DAP035 | Library | Error | Diagnostics -DAP036 | Library | Error | Diagnostics -DAP037 | Library | Error | Diagnostics -DAP038 | Library | Warning | Diagnostics -DAP100 | Library | Error | Diagnostics -DAP101 | Library | Error | Diagnostics -DAP102 | Library | Error | Diagnostics -DAP200 | Sql | Warning | Diagnostics -DAP201 | Sql | Error | Diagnostics -DAP202 | Sql | Error | Diagnostics -DAP203 | Sql | Error | Diagnostics -DAP204 | Sql | Info | Diagnostics -DAP205 | Sql | Warning | Diagnostics -DAP206 | Sql | Error | Diagnostics -DAP207 | Sql | Error | Diagnostics -DAP208 | Sql | Error | Diagnostics -DAP209 | Sql | Error | Diagnostics -DAP210 | Sql | Error | Diagnostics -DAP211 | Sql | Error | Diagnostics -DAP212 | Sql | Warning | Diagnostics -DAP213 | Sql | Warning | Diagnostics -DAP214 | Sql | Error | Diagnostics -DAP215 | Sql | Warning | Diagnostics -DAP216 | Sql | Warning | Diagnostics -DAP217 | Sql | Error | Diagnostics -DAP218 | Sql | Error | Diagnostics -DAP219 | Sql | Warning | Diagnostics -DAP220 | Sql | Warning | Diagnostics -DAP221 | Sql | Warning | Diagnostics -DAP222 | Sql | Warning | Diagnostics -DAP223 | Sql | Warning | Diagnostics -DAP224 | Sql | Warning | Diagnostics diff --git a/src/Dapper.AOT.Analyzers/AnalyzerReleases.Unshipped.md b/src/Dapper.AOT.Analyzers/AnalyzerReleases.Unshipped.md deleted file mode 100644 index cc67b67f..00000000 --- a/src/Dapper.AOT.Analyzers/AnalyzerReleases.Unshipped.md +++ /dev/null @@ -1,7 +0,0 @@ -; Unshipped analyzer release -; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md - -### New Rules - -Rule ID | Category | Severity | Notes ---------|----------|----------|------- diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Diagnostics.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Diagnostics.cs index 16ba84dd..ad4fc38c 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Diagnostics.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Diagnostics.cs @@ -6,149 +6,94 @@ partial class DapperInterceptorGenerator { internal sealed class Diagnostics : DiagnosticsBase { + internal static readonly DiagnosticDescriptor - InterceptorsGenerated = new("DAP000", "Interceptors generated", - "Dapper.AOT handled {0} of {1} enabled call-sites using {2} interceptors, {3} commands and {4} readers", Category.Library, DiagnosticSeverity.Hidden, true), - UnsupportedMethod = new("DAP001", "Unsupported method", - "The Dapper method '{0}' is not currently supported by Dapper.AOT", Category.Library, DiagnosticSeverity.Info, true), + InterceptorsGenerated = LibraryHidden("DAP000", "Interceptors generated", "Dapper.AOT handled {0} of {1} enabled call-sites using {2} interceptors, {3} commands and {4} readers"), + UnsupportedMethod = LibraryInfo("DAP001", "Unsupported method", "The Dapper method '{0}' is not currently supported by Dapper.AOT"), //UntypedResults = new("DAP002", "Untyped result types", // "Dapper.AOT does not currently support untyped/dynamic results", Category.Library, DiagnosticSeverity.Info, true), - InterceptorsNotEnabled = new("DAP003", "Interceptors not enabled", - "Interceptors are an experimental feature, and requires that 'InterceptorsPreview' be added to the project file", Category.Library, DiagnosticSeverity.Warning, true), - LanguageVersionTooLow = new("DAP004", "Language version too low", - "Interceptors require at least C# version 11", Category.Library, DiagnosticSeverity.Warning, true), - DapperAotNotEnabled = new("DAP005", "Dapper.AOT not enabled", - "Candidate Dapper methods were detected, but none have Dapper.AOT enabled; [DapperAot] can be added at the method, type, module or assembly level (for example '[module:DapperAot]')", Category.Library, DiagnosticSeverity.Info, true), - DapperLegacyTupleParameter = new("DAP006", "Dapper tuple-type parameter", - "Dapper (original) does not work well with tuple-type parameters as name information is inaccessible", Category.Library, DiagnosticSeverity.Warning, true), - UnexpectedCommandType = new("DAP007", "Unexpected command type", - "The command type specified is not understood", Category.Library, DiagnosticSeverity.Info, true), + InterceptorsNotEnabled = LibraryWarning("DAP003", "Interceptors not enabled", + "Interceptors need to be enabled (see help-link)", true), + LanguageVersionTooLow = LibraryWarning("DAP004", "Language version too low", "Interceptors require at least C# version 11", true), + DapperAotNotEnabled = LibraryInfo("DAP005", "Dapper.AOT not enabled", + "Candidate Dapper methods were detected, but none have Dapper.AOT enabled; [DapperAot] can be added at the method, type, module or assembly level (for example '[module:DapperAot]')", true), + DapperLegacyTupleParameter = LibraryWarning("DAP006", "Dapper tuple-type parameter", "Dapper (original) does not work well with tuple-type parameters as name information is inaccessible"), + UnexpectedCommandType = LibraryInfo("DAP007", "Unexpected command type", "The command type specified is not understood"), // space - UnexpectedArgument = new("DAP009", "Unexpected parameter", - "The parameter '{0}' is not understood", Category.Library, DiagnosticSeverity.Info, true), + UnexpectedArgument = LibraryInfo("DAP009", "Unexpected parameter", "The parameter '{0}' is not understood"), // space - DapperLegacyBindNameTupleResults = new("DAP011", "Named-tuple results", - "Dapper (original) does not support tuple results with bind-by-name semantics", Category.Library, DiagnosticSeverity.Warning, true), - DapperAotAddBindTupleByName = new("DAP012", "Add BindTupleByName", - "Because of differences in how Dapper and Dapper.AOT can process tuple-types, please add '[BindTupleByName({true|false})]' to clarify your intent", Category.Library, DiagnosticSeverity.Warning, true), - DapperAotTupleResults = new("DAP013", "Tuple-type results", - "Tuple-type results are not currently supported", Category.Library, DiagnosticSeverity.Info, true), - DapperAotTupleParameter = new("DAP014", "Tuple-type parameter", - "Tuple-type parameters are not currently supported", Category.Library, DiagnosticSeverity.Info, true), - UntypedParameter = new("DAP015", "Untyped parameter", - "The parameter type could not be resolved", Category.Library, DiagnosticSeverity.Info, true), - GenericTypeParameter = new("DAP016", "Generic type parameter", - "Generic type parameters ({0}) are not currently supported", Category.Library, DiagnosticSeverity.Info, true), - NonPublicType = new("DAP017", "Non-accessible type", - "Type '{0}' is not accessible; {1} types are not currently supported", Category.Library, DiagnosticSeverity.Info, true), - SqlParametersNotDetected = new("DAP018", "SQL parameters not detected", - "Parameters are being supplied, but no parameters were detected in the command", Category.Sql, DiagnosticSeverity.Warning, true), - NoParametersSupplied = new("DAP019", "No parameters supplied", - "SQL parameters were detected, but no parameters are being supplied", Category.Sql, DiagnosticSeverity.Error, true, helpLinkUri: RulesRoot + "DAP019"), - SqlParameterNotBound = new("DAP020", "SQL parameter not bound", - "No member could be found for the SQL parameter '{0}' from type '{1}'", Category.Sql, DiagnosticSeverity.Error, true), - DuplicateParameter = new("DAP021", "Duplicate parameter", - "Members '{0}' and '{1}' both have the database name '{2}'; '{0}' will be ignored", Category.Sql, DiagnosticSeverity.Warning, true), - DuplicateReturn = new("DAP022", "Duplicate return parameter", - "Members '{0}' and '{1}' are both designated as return values; '{0}' will be ignored", Category.Sql, DiagnosticSeverity.Warning, true), - DuplicateRowCount = new("DAP023", "Duplicate row-count member", - "Members '{0}' and '{1}' are both marked [RowCount]", Category.Sql, DiagnosticSeverity.Warning, true), - RowCountDbValue = new("DAP024", "Member is both row-count and mapped value", - "Member '{0}' is marked both [RowCount] and [DbValue]; [DbValue] will be ignored", Category.Sql, DiagnosticSeverity.Warning, true), - ExecuteCommandWithQuery = new("DAP025", "Execute command with query", - "The command has a query that will be ignored", Category.Sql, DiagnosticSeverity.Warning, true), - QueryCommandMissingQuery = new("DAP026", "Query/scalar command lacks query", - "The command lacks a query", Category.Sql, DiagnosticSeverity.Error, true), - UseSingleRowQuery = new("DAP027", "Use single-row query", - "Use {0}() instead of Query(...).{1}()", Category.Performance, DiagnosticSeverity.Warning, true), - UseQueryAsList = new("DAP028", "Use AsList instead of ToList", - "Use Query(...).AsList() instead of Query(...).ToList()", Category.Performance, DiagnosticSeverity.Warning, true), - MethodRowCountHintRedundant = new("DAP029", "Method-level row-count hint redundant", - "The [EstimatedRowCount] will be ignored due to parameter member '{0}'", Category.Library, DiagnosticSeverity.Info, true), - MethodRowCountHintInvalid = new("DAP030", "Method-level row-count hint invalid", - "The [EstimatedRowCount] parameters are invalid; a positive integer must be supplied", Category.Library, DiagnosticSeverity.Error, true), - MemberRowCountHintInvalid = new("DAP031", "Member-level row-count hint invalid", - "The [EstimatedRowCount] parameters are invalid; no parameter should be supplied", Category.Library, DiagnosticSeverity.Error, true), - MemberRowCountHintDuplicated = new("DAP032", "Member-level row-count hint duplicated", - "Only a single member should be marked [EstimatedRowCount]", Category.Library, DiagnosticSeverity.Error, true), - CommandPropertyNotFound = new("DAP033", "Command property not found", - "Command property {0}.{1} was not found or was not valid; attribute will be ignored", Category.Library, DiagnosticSeverity.Warning, true), - CommandPropertyReserved = new("DAP034", "Command property reserved", - "Command property {1} is reserved for internal usage; attribute will be ignored", Category.Library, DiagnosticSeverity.Warning, true), - TooManyDapperAotEnabledConstructors = new("DAP035", "Too many Dapper.AOT enabled constructors", - "Only one constructor can be Dapper.AOT enabled per type '{0}'", Category.Library, DiagnosticSeverity.Error, true), - TooManyStandardConstructors = new("DAP036", "Type has more than 1 constructor to choose for creating an instance", - "Type has more than 1 constructor, please, either mark one constructor with [DapperAot] or reduce amount of constructors", Category.Library, DiagnosticSeverity.Error, true), - UserTypeNoSettableMembersFound = new("DAP037", "No settable members exist for user type", - "Type '{0}' has no settable members (fields or properties)", Category.Library, DiagnosticSeverity.Error, true), - ValueTypeSingleFirstOrDefaultUsage = new("DAP038", "Value-type single row 'OrDefault' usage", - "Type '{0}' is a value-type; it will not be trivial to identify missing rows from {1}", Category.Library, DiagnosticSeverity.Warning, true), + DapperLegacyBindNameTupleResults = LibraryWarning("DAP011", "Named-tuple results", "Dapper (original) does not support tuple results with bind-by-name semantics"), + DapperAotAddBindTupleByName = LibraryWarning("DAP012", "Add BindTupleByName", "Because of differences in how Dapper and Dapper.AOT can process tuple-types, please add '[BindTupleByName({true|false})]' to clarify your intent"), + DapperAotTupleResults = LibraryInfo("DAP013", "Tuple-type results", "Tuple-type results are not currently supported"), + DapperAotTupleParameter = LibraryInfo("DAP014", "Tuple-type parameter", "Tuple-type parameters are not currently supported"), + UntypedParameter = LibraryInfo("DAP015", "Untyped parameter", "The parameter type could not be resolved"), + GenericTypeParameter = LibraryInfo("DAP016", "Generic type parameter", "Generic type parameters ({0}) are not currently supported"), + NonPublicType = LibraryInfo("DAP017", "Non-accessible type", "Type '{0}' is not accessible; {1} types are not currently supported"), + SqlParametersNotDetected = SqlWarning("DAP018", "SQL parameters not detected", "Parameters are being supplied, but no parameters were detected in the command"), + NoParametersSupplied = SqlWarning("DAP019", "No parameters supplied", "SQL parameters were detected, but no parameters are being supplied", true), + SqlParameterNotBound = SqlWarning("DAP020", "SQL parameter not bound", "No member could be found for the SQL parameter '{0}' from type '{1}'"), + DuplicateParameter = LibraryWarning("DAP021", "Duplicate parameter", "Members '{0}' and '{1}' both have the database name '{2}'; '{0}' will be ignored"), + DuplicateReturn = LibraryWarning("DAP022", "Duplicate return parameter", "Members '{0}' and '{1}' are both designated as return values; '{0}' will be ignored"), + DuplicateRowCount = LibraryWarning("DAP023", "Duplicate row-count member", + "Members '{0}' and '{1}' are both marked [RowCount]"), + RowCountDbValue = LibraryWarning("DAP024", "Member is both row-count and mapped value", + "Member '{0}' is marked both [RowCount] and [DbValue]; [DbValue] will be ignored"), + ExecuteCommandWithQuery = SqlWarning("DAP025", "Execute command with query", "The command has a query that will be ignored"), + QueryCommandMissingQuery = SqlError("DAP026", "Query/scalar command lacks query", "The command lacks a query"), + UseSingleRowQuery = PerformanceWarning("DAP027", "Use single-row query", "Use {0}() instead of Query(...).{1}()"), + UseQueryAsList = PerformanceWarning("DAP028", "Use AsList instead of ToList", "Use Query(...).AsList() instead of Query(...).ToList()"), + MethodRowCountHintRedundant = LibraryInfo("DAP029", "Method-level row-count hint redundant", "The [EstimatedRowCount] will be ignored due to parameter member '{0}'"), + MethodRowCountHintInvalid = LibraryError("DAP030", "Method-level row-count hint invalid", + "The [EstimatedRowCount] parameters are invalid; a positive integer must be supplied"), + MemberRowCountHintInvalid = LibraryError("DAP031", "Member-level row-count hint invalid", + "The [EstimatedRowCount] parameters are invalid; no parameter should be supplied"), + MemberRowCountHintDuplicated = LibraryError("DAP032", "Member-level row-count hint duplicated", + "Only a single member should be marked [EstimatedRowCount]"), + CommandPropertyNotFound = LibraryWarning("DAP033", "Command property not found", + "Command property {0}.{1} was not found or was not valid; attribute will be ignored"), + CommandPropertyReserved = LibraryWarning("DAP034", "Command property reserved", + "Command property {1} is reserved for internal usage; attribute will be ignored"), + TooManyDapperAotEnabledConstructors = LibraryError("DAP035", "Too many Dapper.AOT enabled constructors", + "Only one constructor can be Dapper.AOT enabled per type '{0}'"), + TooManyStandardConstructors = LibraryError("DAP036", "Type has more than 1 constructor to choose for creating an instance", + "Type has more than 1 constructor, please, either mark one constructor with [DapperAot] or reduce amount of constructors"), + UserTypeNoSettableMembersFound = LibraryError("DAP037", "No settable members exist for user type", + "Type '{0}' has no settable members (fields or properties)"), + ValueTypeSingleFirstOrDefaultUsage = LibraryWarning("DAP038", "Value-type single row 'OrDefault' usage", + "Type '{0}' is a value-type; it will not be trivial to identify missing rows from {1}"), // SQL parse specific - SqlError = new("DAP200", "SQL error", - "SQL error: {0}", Category.Sql, DiagnosticSeverity.Warning, true), - MultipleBatches = new("DAP201", "Multiple batches", - "Multiple batches are not permitted (L{0} C{1})", Category.Sql, DiagnosticSeverity.Error, true), - DuplicateVariableDeclaration = new("DAP202", "Duplicate variable declaration", - "The variable {0} is declared multiple times (L{1} C{2})", Category.Sql, DiagnosticSeverity.Error, true), - GlobalIdentity = new("DAP203", "Do not use @@identity", - "@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L{0} C{1})", Category.Sql, DiagnosticSeverity.Error, true), - SelectScopeIdentity = new("DAP204", "Prefer OUTPUT over SELECT", - "Consider using OUTPUT INSERTED.yourid in the INSERT instead of SELECT SCOPE_IDENTITY() (L{0} C{1})", Category.Sql, DiagnosticSeverity.Info, true), - NullLiteralComparison = new("DAP205", "Null comparison", - "Literal null used in comparison; 'is null' or 'is not null' should be preferred (L{0} C{1})", Category.Sql, DiagnosticSeverity.Warning, true), - ParseError = new("DAP206", "SQL parse error", - "{0} (#{1} L{2} C{3})", Category.Sql, DiagnosticSeverity.Error, true), - ScalarVariableUsedAsTable = new("DAP207", "Scalar used like table", - "Scalar variable {0} is used like a table (L{1} C{2})", Category.Sql, DiagnosticSeverity.Error, true), - TableVariableUsedAsScalar = new("DAP208", "Table used like scalar", - "Table-variable {0} is used like a scalar (L{1} C{2})", Category.Sql, DiagnosticSeverity.Error, true), - TableVariableAccessedBeforePopulate = new("DAP209", "Table used before populate", - "Table-variable {0} is accessed before it populated (L{1} C{2})", Category.Sql, DiagnosticSeverity.Error, true), - VariableAccessedBeforeAssignment = new("DAP210", "Variable used before assigned", - "Variable {0} is accessed before it is assigned a value (L{1} C{2})", Category.Sql, DiagnosticSeverity.Error, true), - VariableAccessedBeforeDeclaration = new("DAP211", "Variable used before declared", - "Variable {0} is accessed before it is declared (L{1} C{2})", Category.Sql, DiagnosticSeverity.Error, true), - ExecVariable = new("DAP212", "EXEC with composed SQL", - "EXEC with composed SQL may be susceptible to SQL injection; consider EXEC sp_executesql, taking care to fully parameterize the composed query (L{0} C{1})", Category.Sql, DiagnosticSeverity.Warning, true), - VariableValueNotConsumed = new("DAP213", "Variable used before declared", - "Variable {0} has a value that is not consumed (L{1} C{2})", Category.Sql, DiagnosticSeverity.Warning, true), - VariableNotDeclared = new("DAP214", "Variable not declared", - "Variable {0} is not declared and no corresponding parameter exists (L{1} C{2})", Category.Sql, DiagnosticSeverity.Error, true), - TableVariableOutputParameter = new("DAP215", "Variable used before declared", - "Table variable {0} cannot be used as an output parameter (L{1} C{2})", Category.Sql, DiagnosticSeverity.Warning, true), - InsertColumnsNotSpecified = new("DAP216", "INSERT without target columns", - "INSERT should explicitly specify target columns (L{0} C{1})", Category.Sql, DiagnosticSeverity.Warning, true), - InsertColumnsMismatch = new("DAP217", "INSERT with mismatched columns", - "The INSERT values do not match the target columns (L{0} C{1})", Category.Sql, DiagnosticSeverity.Error, true), - InsertColumnsUnbalanced = new("DAP218", "INSERT with unbalanced rows", - "The INSERT rows have different widths (L{0} C{1})", Category.Sql, DiagnosticSeverity.Error, true), - SelectStar = new("DAP219", "SELECT with wildcard columns", - "SELECT columns should be specified explicitly (L{0} C{1})", Category.Sql, DiagnosticSeverity.Warning, true), - SelectEmptyColumnName = new("DAP220", "SELECT with missing column name", - "SELECT column name is missing: {0} (L{1} C{2})", Category.Sql, DiagnosticSeverity.Warning, true), - SelectDuplicateColumnName = new("DAP221", "SELECT with duplicate column name", - "SELECT column name is duplicated: '{0}' (L{1} C{2})", Category.Sql, DiagnosticSeverity.Warning, true), - SelectAssignAndRead = new("DAP222", "SELECT with assignment and reads", - "SELECT statement assigns variable and performs reads (L{0} C{1})", Category.Sql, DiagnosticSeverity.Warning, true), - DeleteWithoutWhere = new("DAP223", "DELETE without WHERE", - "DELETE statement lacks WHERE clause (L{0} C{1})", Category.Sql, DiagnosticSeverity.Warning, true), - UpdateWithoutWhere = new("DAP224", "UPDATE without WHERE", - "UPDATE statement lacks WHERE clause (L{0} C{1})", Category.Sql, DiagnosticSeverity.Warning, true), - - FromMultiTableMissingAlias = new("DAP225", "Multi-element FROM missing alias", - "FROM expressions with multiple elements should use aliases (L{0} C{1})", Category.Sql, DiagnosticSeverity.Warning, true), - FromMultiTableUnqualifiedColumn = new("DAP226", "Multi-element FROM with unqualified column", - "FROM expressions with multiple elements should qualify all columns; it is unclear where '{0}' is located (L{1} C{2})", Category.Sql, DiagnosticSeverity.Warning, true), - NonIntegerTop = new("DAP227", "Non-integer TOP", - "TOP literals should be integers (L{0} C{1})", Category.Sql, DiagnosticSeverity.Error, true), - NonPositiveTop = new("DAP228", "Non-positive TOP", - "TOP literals should be positive (L{0} C{1})", Category.Sql, DiagnosticSeverity.Error, true), - SelectFirstTopError = new("DAP229", "SELECT for First* with invalid TOP", - "SELECT for First* should use TOP 1 (L{0} C{1})", Category.Sql, DiagnosticSeverity.Warning, true), - SelectSingleTopError = new("DAP230", "SELECT for Single* with invalid TOP", - "SELECT for Single* should use TOP 2; if you do not need to test over-read, use First* (L{0} C{1})", Category.Sql, DiagnosticSeverity.Warning, true), - SelectSingleRowWithoutWhere = new("DAP231", "SELECT for single row without WHERE", - "SELECT for single row without WHERE or (TOP and ORDER BY) (L{0} C{1})", Category.Sql, DiagnosticSeverity.Warning, true); + GeneralSqlError = SqlWarning("DAP200", "SQL error", "SQL error: {0}"), + MultipleBatches = SqlError("DAP201", "Multiple batches", "Multiple batches are not permitted (L{0} C{1})"), + DuplicateVariableDeclaration = SqlError("DAP202", "Duplicate variable declaration", "The variable {0} is declared multiple times (L{1} C{2})"), + GlobalIdentity = SqlError("DAP203", "Do not use @@identity", "@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L{0} C{1})"), + SelectScopeIdentity = SqlInfo("DAP204", "Prefer OUTPUT over SELECT", "Consider using OUTPUT INSERTED.yourid in the INSERT instead of SELECT SCOPE_IDENTITY() (L{0} C{1})"), + NullLiteralComparison = SqlWarning("DAP205", "Null comparison", "Literal null used in comparison; 'is null' or 'is not null' should be preferred (L{0} C{1})"), + ParseError = SqlError("DAP206", "SQL parse error", "{0} (#{1} L{2} C{3})"), + ScalarVariableUsedAsTable = SqlError("DAP207", "Scalar used like table", "Scalar variable {0} is used like a table (L{1} C{2})"), + TableVariableUsedAsScalar = SqlError("DAP208", "Table used like scalar", "Table-variable {0} is used like a scalar (L{1} C{2})"), + TableVariableAccessedBeforePopulate = SqlError("DAP209", "Table used before populate", "Table-variable {0} is accessed before it populated (L{1} C{2})"), + VariableAccessedBeforeAssignment = SqlError("DAP210", "Variable used before assigned", "Variable {0} is accessed before it is assigned a value (L{1} C{2})"), + VariableAccessedBeforeDeclaration = SqlError("DAP211", "Variable used before declared", "Variable {0} is accessed before it is declared (L{1} C{2})"), + ExecVariable = SqlWarning("DAP212", "EXEC with composed SQL", "EXEC with composed SQL may be susceptible to SQL injection; consider EXEC sp_executesql, taking care to fully parameterize the composed query (L{0} C{1})"), + VariableValueNotConsumed = SqlWarning("DAP213", "Variable used before declared", "Variable {0} has a value that is not consumed (L{1} C{2})"), + VariableNotDeclared = SqlError("DAP214", "Variable not declared", "Variable {0} is not declared and no corresponding parameter exists (L{1} C{2})"), + TableVariableOutputParameter = SqlWarning("DAP215", "Variable used before declared", "Table variable {0} cannot be used as an output parameter (L{1} C{2})"), + InsertColumnsNotSpecified = SqlWarning("DAP216", "INSERT without target columns", "INSERT should explicitly specify target columns (L{0} C{1})"), + InsertColumnsMismatch = SqlError("DAP217", "INSERT with mismatched columns", "The INSERT values do not match the target columns (L{0} C{1})"), + InsertColumnsUnbalanced = SqlError("DAP218", "INSERT with unbalanced rows", "The INSERT rows have different widths (L{0} C{1})"), + SelectStar = SqlWarning("DAP219", "SELECT with wildcard columns", "SELECT columns should be specified explicitly (L{0} C{1})"), + SelectEmptyColumnName = SqlWarning("DAP220", "SELECT with missing column name", "SELECT column name is missing: {0} (L{1} C{2})"), + SelectDuplicateColumnName = SqlWarning("DAP221", "SELECT with duplicate column name", "SELECT column name is duplicated: '{0}' (L{1} C{2})"), + SelectAssignAndRead = SqlWarning("DAP222", "SELECT with assignment and reads", "SELECT statement assigns variable and performs reads (L{0} C{1})"), + DeleteWithoutWhere = SqlWarning("DAP223", "DELETE without WHERE", "DELETE statement lacks WHERE clause (L{0} C{1})"), + UpdateWithoutWhere = SqlWarning("DAP224", "UPDATE without WHERE", "UPDATE statement lacks WHERE clause (L{0} C{1})"), + FromMultiTableMissingAlias = SqlWarning("DAP225", "Multi-element FROM missing alias", "FROM expressions with multiple elements should use aliases (L{0} C{1})"), + FromMultiTableUnqualifiedColumn = SqlWarning("DAP226", "Multi-element FROM with unqualified column", "FROM expressions with multiple elements should qualify all columns; it is unclear where '{0}' is located (L{1} C{2})"), + NonIntegerTop = SqlError("DAP227", "Non-integer TOP", "TOP literals should be integers (L{0} C{1})"), + NonPositiveTop = SqlError("DAP228", "Non-positive TOP", "TOP literals should be positive (L{0} C{1})"), + SelectFirstTopError = SqlWarning("DAP229", "SELECT for First* with invalid TOP", "SELECT for First* should use TOP 1 (L{0} C{1})"), + SelectSingleTopError = SqlWarning("DAP230", "SELECT for Single* with invalid TOP", "SELECT for Single* should use TOP 2; if you do not need to test over-read, use First* (L{0} C{1})"), + SelectSingleRowWithoutWhere = SqlWarning("DAP231", "SELECT for single row without WHERE", "SELECT for single row without WHERE or (TOP and ORDER BY) (L{0} C{1})"); } } diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs index f05f0372..4f6b2f58 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs @@ -481,7 +481,7 @@ select var.Name.StartsWith("@") ? var.Name.Substring(1) : var.Name } catch (Exception ex) { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.SqlError, loc, ex.Message)); + Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.GeneralSqlError, loc, ex.Message)); goto default; // some internal failure } break; diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/Diagnostics.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/Diagnostics.cs index 0b413ddf..eb245be8 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/Diagnostics.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/Diagnostics.cs @@ -11,6 +11,30 @@ internal abstract class DiagnosticsBase { protected const string DocsRoot = "https://aot.dapperlib.dev/", RulesRoot = DocsRoot + "rules/"; + private static DiagnosticDescriptor Create(string id, string title, string messageFormat, string? category, DiagnosticSeverity severity, bool docs) => + new(id, title, + messageFormat, category, severity, true, helpLinkUri: docs ? (RulesRoot + id) : null); + + protected static DiagnosticDescriptor LibraryWarning(string id, string title, string messageFormat, bool docs = false) => Create(id, title, messageFormat, Category.Library, DiagnosticSeverity.Warning, docs); + + protected static DiagnosticDescriptor LibraryError(string id, string title, string messageFormat, bool docs = false) => Create(id, title, messageFormat, Category.Library, DiagnosticSeverity.Error, docs); + + protected static DiagnosticDescriptor LibraryHidden(string id, string title, string messageFormat, bool docs = false) => Create(id, title, messageFormat, Category.Library, DiagnosticSeverity.Hidden, docs); + + protected static DiagnosticDescriptor LibraryInfo(string id, string title, string messageFormat, bool docs = false) => Create(id, title, messageFormat, Category.Library, DiagnosticSeverity.Info, docs); + + protected static DiagnosticDescriptor SqlWarning(string id, string title, string messageFormat, bool docs = false) => Create(id, title, messageFormat, Category.Sql, DiagnosticSeverity.Warning, docs); + + protected static DiagnosticDescriptor SqlError(string id, string title, string messageFormat, bool docs = false) => Create(id, title, messageFormat, Category.Sql, DiagnosticSeverity.Error, docs); + + protected static DiagnosticDescriptor SqlInfo(string id, string title, string messageFormat, bool docs = false) => Create(id, title, messageFormat, Category.Sql, DiagnosticSeverity.Info, docs); + + protected static DiagnosticDescriptor PerformanceWarning(string id, string title, string messageFormat, bool docs = false) => Create(id, title, messageFormat, Category.Performance, DiagnosticSeverity.Warning, docs); + + protected static DiagnosticDescriptor PerformanceError(string id, string title, string messageFormat, bool docs = false) => Create(id, title, messageFormat, Category.Performance, DiagnosticSeverity.Error, docs); + + protected static DiagnosticDescriptor PerformanceInfo(string id, string title, string messageFormat, bool docs = false) => Create(id, title, messageFormat, Category.Performance, DiagnosticSeverity.Info, docs); + private static ImmutableDictionary? _idsToFieldNames; public static bool TryGetFieldName(string id, out string field) { @@ -44,7 +68,7 @@ public static readonly ImmutableArray All } - protected static class Category + private static class Category { public const string Library = nameof(Library); public const string Sql = nameof(Sql); diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/TypeAccessorInterceptorGenerator.Diagnostics.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/TypeAccessorInterceptorGenerator.Diagnostics.cs index 5e4c35ec..550b8483 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/TypeAccessorInterceptorGenerator.Diagnostics.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/TypeAccessorInterceptorGenerator.Diagnostics.cs @@ -8,12 +8,12 @@ internal sealed class Diagnostics : DiagnosticsBase { internal static readonly DiagnosticDescriptor // TypeAccessor - TypeAccessorCollectionTypeNotAllowed = new("DAP100", "TypeAccessors does not allow collection types", - "TypeAccessors does not allow collection types", Category.Library, DiagnosticSeverity.Error, true), - TypeAccessorPrimitiveTypeNotAllowed = new("DAP101", "TypeAccessors does not allow primitive types", - "TypeAccessors does not allow primitive types", Category.Library, DiagnosticSeverity.Error, true), - TypeAccessorMembersNotParsed = new("DAP102", "TypeAccessor members can not be parsed", - "At least one gettable and settable member must be defined for type '{0}'", Category.Library, DiagnosticSeverity.Error, true); + TypeAccessorCollectionTypeNotAllowed = LibraryError("DAP100", "TypeAccessors does not allow collection types", + "TypeAccessors does not allow collection types"), + TypeAccessorPrimitiveTypeNotAllowed = LibraryError("DAP101", "TypeAccessors does not allow primitive types", + "TypeAccessors does not allow primitive types"), + TypeAccessorMembersNotParsed = LibraryError("DAP102", "TypeAccessor members can not be parsed", + "At least one gettable and settable member must be defined for type '{0}'"); } } diff --git a/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj b/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj index 2f3a5a89..3341a58d 100644 --- a/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj +++ b/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj @@ -10,6 +10,8 @@ true true true + diff --git a/src/Dapper.AOT.Analyzers/Internal/DiagnosticTSqlProcessor.cs b/src/Dapper.AOT.Analyzers/Internal/DiagnosticTSqlProcessor.cs index 32ebbb27..c63f8da7 100644 --- a/src/Dapper.AOT.Analyzers/Internal/DiagnosticTSqlProcessor.cs +++ b/src/Dapper.AOT.Analyzers/Internal/DiagnosticTSqlProcessor.cs @@ -73,7 +73,7 @@ private void AddDiagnostic(DiagnosticDescriptor diagnostic, in Location location protected override void OnError(string error, in Location location) { Debug.Fail("unhandled error: " + error); - AddDiagnostic(DapperInterceptorGenerator.Diagnostics.SqlError, location, error, location.Line, location.Column); + AddDiagnostic(DapperInterceptorGenerator.Diagnostics.GeneralSqlError, location, error, location.Line, location.Column); } protected override void OnAdditionalBatch(Location location) diff --git a/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj b/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj index dad11f4b..10beaa55 100644 --- a/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj +++ b/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj @@ -1,6 +1,6 @@  - net6.0;net7.0;net8.0;net48 + net6.0;net48 $(NoWarn);IDE0042;CS8002 Dapper.AOT.Test $(DefineConstants);DAPPERAOT_INTERNAL diff --git a/test/Dapper.AOT.Test/Helpers/AnalyzerAndCodeFixVerifier.cs b/test/Dapper.AOT.Test/Helpers/AnalyzerAndCodeFixVerifier.cs deleted file mode 100644 index 3fce5aee..00000000 --- a/test/Dapper.AOT.Test/Helpers/AnalyzerAndCodeFixVerifier.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Microsoft.CodeAnalysis.CodeFixes; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Testing; -using Microsoft.CodeAnalysis.Testing.Verifiers; -using System.Data.SqlClient; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -// credit: https://www.thinktecture.com/en/net/roslyn-source-generators-analyzers-code-fixes-testing/ - -namespace Dapper.AOT.Test; - -public static class AnalyzerAndCodeFixVerifier - where TAnalyzer : DiagnosticAnalyzer, new() - where TCodeFix : CodeFixProvider, new() -{ - public static DiagnosticResult Diagnostic(string diagnosticId) - { - return CSharpCodeFixVerifier - .Diagnostic(diagnosticId); - } - - public static async Task VerifyCodeFixAsync( - string source, - string fixedSource, - params DiagnosticResult[] expected) - { - var test = new CodeFixTest(source, fixedSource, expected); - await test.RunAsync(CancellationToken.None); - } - - private class CodeFixTest : CSharpCodeFixTest - { - public CodeFixTest( - string source, - string fixedSource, - params DiagnosticResult[] expected) - { - TestCode = source; - FixedCode = fixedSource; - ExpectedDiagnostics.AddRange(expected); -#if NETFRAMEWORK - ReferenceAssemblies = ReferenceAssemblies.NetFramework.Net472.Default; -#else - ReferenceAssemblies = ReferenceAssemblies.Net.Net60; -#endif - - TestState.AdditionalReferences.Add(typeof(SqlMapper).Assembly); - TestState.AdditionalReferences.Add(typeof(DapperAotAttribute).Assembly); - } - } -} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Helpers/AnalyzerVerifier.cs b/test/Dapper.AOT.Test/Helpers/AnalyzerVerifier.cs deleted file mode 100644 index 8c999b4a..00000000 --- a/test/Dapper.AOT.Test/Helpers/AnalyzerVerifier.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Testing; -using Microsoft.CodeAnalysis.Testing.Verifiers; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -// credit: https://www.thinktecture.com/en/net/roslyn-source-generators-analyzers-code-fixes-testing/ - -namespace Dapper.AOT.Test; - -public static class AnalyzerVerifier - where TAnalyzer : DiagnosticAnalyzer, new() -{ - public static DiagnosticResult Diagnostic(string diagnosticId) - { - return CSharpAnalyzerVerifier.Diagnostic(diagnosticId); - } - - public static async Task VerifyAnalyzerAsync( - string source, - params DiagnosticResult[] expected) - { - var test = new AnalyzerTest(source, expected); - await test.RunAsync(CancellationToken.None); - } - - private class AnalyzerTest : CSharpAnalyzerTest - { - public AnalyzerTest( - string source, - params DiagnosticResult[] expected) - { - TestCode = source; - ExpectedDiagnostics.AddRange(expected); -#if NETFRAMEWORK - ReferenceAssemblies = ReferenceAssemblies.NetFramework.Net472.Default; -#else - ReferenceAssemblies = ReferenceAssemblies.Net.Net60; -#endif - - TestState.AdditionalReferences.Add(typeof(SqlMapper).Assembly); - TestState.AdditionalReferences.Add(typeof(DapperAotAttribute).Assembly); - } - } -} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.netfx.txt index 457a6072..5856fae6 100644 --- a/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.netfx.txt +++ b/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.netfx.txt @@ -3,7 +3,7 @@ Generator produced 4 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 7 of 7 enabled call-sites using 5 interceptors, 2 commands and 0 readers -Error DAP019 Interceptors/SqlDetection.input.cs L20 C20 +Warning DAP019 Interceptors/SqlDetection.input.cs L20 C20 SQL parameters were detected, but no parameters are being supplied Warning DAP018 Interceptors/SqlDetection.input.cs L23 C20 diff --git a/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.txt b/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.txt index b9fe470f..c2092697 100644 --- a/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.txt @@ -3,7 +3,7 @@ Generator produced 4 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 7 of 7 enabled call-sites using 5 interceptors, 2 commands and 0 readers -Error DAP019 Interceptors/SqlDetection.input.cs L20 C20 +Warning DAP019 Interceptors/SqlDetection.input.cs L20 C20 SQL parameters were detected, but no parameters are being supplied Warning DAP018 Interceptors/SqlDetection.input.cs L23 C20 diff --git a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.txt index 3b274d01..ffff23f7 100644 --- a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.txt +++ b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.txt @@ -141,5 +141,5 @@ SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) Warning DAP231 Interceptors/TsqlTips.input.cs L163 C43 SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) -Error DAP019 Interceptors/TsqlTips.input.cs L173 C17 +Warning DAP019 Interceptors/TsqlTips.input.cs L173 C17 SQL parameters were detected, but no parameters are being supplied diff --git a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.txt b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.txt index 3b274d01..ffff23f7 100644 --- a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.txt @@ -141,5 +141,5 @@ SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) Warning DAP231 Interceptors/TsqlTips.input.cs L163 C43 SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) -Error DAP019 Interceptors/TsqlTips.input.cs L173 C17 +Warning DAP019 Interceptors/TsqlTips.input.cs L173 C17 SQL parameters were detected, but no parameters are being supplied diff --git a/test/Dapper.AOT.Test/TestCommon/GeneratorWrapper.cs b/test/Dapper.AOT.Test/TestCommon/GeneratorWrapper.cs index ac521d07..a450405d 100644 --- a/test/Dapper.AOT.Test/TestCommon/GeneratorWrapper.cs +++ b/test/Dapper.AOT.Test/TestCommon/GeneratorWrapper.cs @@ -1,17 +1,14 @@ using Dapper.CodeAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; -using System; using System.Collections.Concurrent; using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; using static Dapper.CodeAnalysis.DapperInterceptorGenerator; namespace Dapper.AOT.Test.TestCommon; [DiagnosticAnalyzer(LanguageNames.CSharp)] -internal sealed class WrappedDapperInterceptorAnalyzer : DiagnosticAnalyzer +public sealed class WrappedDapperInterceptorAnalyzer : DiagnosticAnalyzer { private readonly DapperInterceptorGenerator inner = new(); diff --git a/test/Dapper.AOT.Test/Verifiers/DAP004.cs b/test/Dapper.AOT.Test/Verifiers/DAP004.cs new file mode 100644 index 00000000..2ea0046b --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP004.cs @@ -0,0 +1,42 @@ +using Dapper.AOT.Test.TestCommon; +using Dapper.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System.Threading.Tasks; +using Xunit; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP004 : Verifier +{ + [Fact] + public Task LanguageTooLow() => VerifyAsync(""" +using Dapper; +using System.Data.Common; + +[DapperAot(true)] +class SomeCode +{ + public void Foo(DbConnection conn) => conn.Execute("some sql"); +} +""", new[] + { + InterceptorsEnabled, WithLanguageVersion(LanguageVersion.CSharp10) + }, Diagnostic(DapperInterceptorGenerator.Diagnostics.LanguageVersionTooLow)); + + [Fact] + public Task FineIfInactive() => VerifyAsync(""" +using Dapper; +using System.Data.Common; + +[DapperAot(false)] +class SomeCode +{ + public void Foo(DbConnection conn) => conn.Execute("some sql"); +} +""", new[] + { + InterceptorsEnabled, WithLanguageVersion(LanguageVersion.CSharp10) + }); + + // we don't need to test higher-level language versions: *every other test does that!* +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP005.cs b/test/Dapper.AOT.Test/Verifiers/DAP005.cs index 25e5b3d6..e76d54ab 100644 --- a/test/Dapper.AOT.Test/Verifiers/DAP005.cs +++ b/test/Dapper.AOT.Test/Verifiers/DAP005.cs @@ -1,17 +1,14 @@ -using Dapper.CodeAnalysis; +using Dapper.AOT.Test.TestCommon; +using Dapper.CodeAnalysis; using System.Threading.Tasks; using Xunit; -// using Verifier = Dapper.AOT.Test.AnalyzerAndCodeFixVerifier; -using Verifier = Dapper.AOT.Test.AnalyzerVerifier; namespace Dapper.AOT.Test.Verifiers; -public class DAP005 +public class DAP005 : Verifier { [Fact] - public async Task ShouldDetectNotEnabledWhenUsed() - { - var input = """ + public Task ShouldFlagWhenUsedAndNotAttrib() => VerifyAsync(""" using Dapper; using System.Data.Common; @@ -19,9 +16,40 @@ class SomeCode { public void Foo(DbConnection conn) => conn.Execute("some sql"); } -"""; +""", Diagnostic(DapperInterceptorGenerator.Diagnostics.DapperAotNotEnabled)); - var expectedError = Verifier.Diagnostic(DapperInterceptorGenerator.Diagnostics.DapperAotNotEnabled.Id); - await Verifier.VerifyAnalyzerAsync(input, expectedError); - } + [Fact] + public Task ShouldNotFlagWhenNotUsedAndNoAttrib() => VerifyAsync(""" +using Dapper; +using System.Data.Common; + +class SomeCode +{ + public void Foo(DbConnection conn) {} +} +"""); + + [Fact] + public Task ShouldNotFlagWhenUsedAndOptedOut() => VerifyAsync(""" +using Dapper; +using System.Data.Common; + +[DapperAot(false)] +class SomeCode +{ + public void Foo(DbConnection conn) => conn.Execute("some sql"); +} +"""); + + [Fact] + public Task ShouldNotFlagWhenUsedAndOptedIn() => VerifyAsync(""" +using Dapper; +using System.Data.Common; + +[DapperAot(true)] +class SomeCode +{ + public void Foo(DbConnection conn) => conn.Execute("some sql"); +} +""", InterceptorsGenerated(1, 1, 1, 0, 0)); } \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/Verifier.cs b/test/Dapper.AOT.Test/Verifiers/Verifier.cs new file mode 100644 index 00000000..ce9023b0 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/Verifier.cs @@ -0,0 +1,123 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Dapper.AOT.Test.Verifiers; +// inspiration: https://www.thinktecture.com/en/net/roslyn-source-generators-analyzers-code-fixes-testing/ +public abstract class Verifier +{ + public CancellationToken CancellationToken { get; private set; } + + protected static DiagnosticResult Diagnostic(DiagnosticDescriptor diagnostic) + => new DiagnosticResult(diagnostic); + protected static DiagnosticResult InterceptorsNotEnabled = Diagnostic(CodeAnalysis.DapperInterceptorGenerator.Diagnostics.InterceptorsNotEnabled); + + protected static DiagnosticResult InterceptorsGenerated(int handled, int total, + int interceptors, int commands, int readers) + => new DiagnosticResult(CodeAnalysis.DapperInterceptorGenerator.Diagnostics.InterceptorsGenerated) + .WithArguments(handled, total, interceptors, commands, readers); + + public Task VerifyAsync(string source, + Func[] transforms, + params DiagnosticResult[] expected) + where TAnalyzer : DiagnosticAnalyzer, new() + { + var test = new CSharpAnalyzerTest(); + return ExecuteAsync(test, source, transforms, expected); + } + public Task VerifyAsync(string source, + Func[] transforms, + params DiagnosticResult[] expected) + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFixProvider : CodeFixProvider, new() + { + var test = new CSharpAnalyzerTest(); + return ExecuteAsync(test, source, transforms, expected); + } + + protected Task ExecuteAsync(AnalyzerTest test, string source, + Func[] transforms, + DiagnosticResult[] expected) + { + test.TestCode = source; + test.ExpectedDiagnostics.AddRange(expected); +#if NETFRAMEWORK + test.ReferenceAssemblies = ReferenceAssemblies.NetFramework.Net472.Default; +#elif NET8_0_OR_GREATER + test.ReferenceAssemblies = new ReferenceAssemblies("net8.0", + new PackageIdentity("Microsoft.NETCore.App.Ref", "8.0.0"), + Path.Combine("ref", "net8.0")); +#elif NET7_0_OR_GREATER + test.ReferenceAssemblies = new ReferenceAssemblies("net7.0", + new PackageIdentity("Microsoft.NETCore.App.Ref", "7.0.0"), + Path.Combine("ref", "net7.0")); +#else + test.ReferenceAssemblies = ReferenceAssemblies.Net.Net60; +#endif + test.TestState.AdditionalReferences.Add(typeof(SqlMapper).Assembly); + test.TestState.AdditionalReferences.Add(typeof(DapperAotAttribute).Assembly); + if (transforms is not null) test.SolutionTransforms.AddRange(transforms); + return test.RunAsync(CancellationToken); + } + protected static Func InterceptorsEnabled = WithFeatures( + new("InterceptorsPreview", "true"), // rc 1 + new("InterceptorsPreviewNamespaces", "Dapper.AOT") // rc2 ? + ); + protected static Func CSharpPreview = WithLanguageVersion(LanguageVersion.Preview); + + protected static Func[] DefaultConfig = new[] { InterceptorsEnabled, CSharpPreview }; + + protected static Func WithLanguageVersion(LanguageVersion version) + => WithParseOptions(options => options.WithLanguageVersion(version)); + protected static Func WithFeatures(params KeyValuePair[] features) + => WithParseOptions(options => options.WithFeatures(features)); + protected static Func WithParseOptions(Func func) + { + if (func is null) return static (solution, _) => solution; + return (solution, projectId) => + { + var options = solution.GetProject(projectId)?.ParseOptions as CSharpParseOptions; + if (options is null) return solution; + return solution.WithProjectParseOptions(projectId, func(options)); + }; + } + +} + +public class Verifier : Verifier where TAnalyzer : DiagnosticAnalyzer, new() +{ + protected Task VerifyAsync(string source, + Func[] transforms, + params DiagnosticResult[] expected) + => base.VerifyAsync(source, transforms, expected); + protected Task VerifyAsync(string source, params DiagnosticResult[] expected) + => base.VerifyAsync(source, DefaultConfig, expected); + + new protected Task VerifyAsync(string source, + Func[] transforms, + params DiagnosticResult[] expected) + where TCodeFixProvider : CodeFixProvider, new() + => VerifyAsync(source, transforms, expected); + + protected Task VerifyAsync(string source, params DiagnosticResult[] expected) + where TCodeFixProvider : CodeFixProvider, new() + => VerifyAsync(source, DefaultConfig, expected); +} +public class Verifier : Verifier + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFixProvider : CodeFixProvider, new() +{ + protected Task VerifyAsync(string source, params DiagnosticResult[] expected) + => VerifyAsync(source, DefaultConfig, expected); + protected Task VerifyAsync(string source, Func[] transforms, + params DiagnosticResult[] expected) + => VerifyAsync(source, transforms, expected); +}