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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions docs/diagnostics/PX1065.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,26 @@ This document describes the PX1065 diagnostic.

## Summary

| Code | Short Description | Type | Code Fix |
| ------ | ----------------------------------------------------------------------------------------- | ------------------------------ | ----------- |
| PX1065 | The DAC field property does not have a corresponding BQL field. | Error | Available |
| Code | Short Description | Type | Code Fix |
| ------ | --------------------------------------------------------------- | ----- | ----------- |
| PX1065 | The DAC field property does not have a corresponding BQL field. | Error | Available |

## Diagnostic Description

Acumatica Framework requires that all DAC field properties have a corresponding BQL field (a public abstract class) in the DAC. A missing BQL field may cause runtime errors or unexpected behavior in numerous places in the application code of Acumatica ERP.
Acumatica Framework requires that all DAC field properties have a corresponding BQL field (a public abstract class) in the DAC. A missing BQL field may cause runtime errors or unexpected behavior
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@EAndrosova please review changes

in numerous places in the application code of Acumatica ERP.

The PX1065 diagnostic checks whether the DAC field property has a corresponding BQL field in the definition of the DAC or in its base DACs.
The PX1065 diagnostic checks whether the declared DAC field property has a corresponding BQL field in the definition of the DAC or in its base DACs.

For DAC extensions, the diagnostic checks whether the DAC field property has a corresponding BQL field in the following locations:
- The DAC extension itself and the base DACs from which it is derived
- The base DAC of the DAC extension and the DACs from which the base DAC has been derived
- The lower-level DAC extensions to which the DAC extension is chained

If a base DAC has a DAC field property without a corresponding BQL field, the PX1065 diagnostic will report an error only if the property is overridden in the derived DAC or redeclared in the DAC extension.



### Terminology

> [!NOTE]
Expand Down Expand Up @@ -76,4 +81,4 @@ public class DAC : PXBqlTable, IBqlTable
- [Data Querying](https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=9241a976-e062-4978-be14-2c1135642be2)
- [Data Access Classes in Traditional BQL](https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=a47ddb36-eb85-486f-9d6b-49beac42fc80)
- [Data Field](https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=b3d24079-bda4-4f82-9fbd-c444a8bcb733)
- [DAC Extensions](https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=114ae5af-8667-4933-b53d-c4c8667c85ac)
- [DAC Extensions](https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=114ae5af-8667-4933-b53d-c4c8667c85ac)
17 changes: 16 additions & 1 deletion docs/diagnostics/PX1080.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@ This document describes the PX1080 diagnostic.
| PX1080 | Data view delegates should not start long-running operations. | Error | Unavailable |

## Diagnostic Description
Data view delegates should not start long-running operations. A data view delegate is designed to prepare a data set to display it in the UI. The result of the data view delegate is returned before the end of the long-running operation.
Data view delegates should not start long-running operations. A data view delegate is designed to prepare a data set to display it in the UI. If the results of the long-running operation are not awaited synchronously,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@EAndrosova please review the updates in PX1080

then the results of the data view delegate are returned before the end of the long-running operation. This scenario is not supported by the Acumatica Framework and may lead to unexpected behavior.

However, even if the results of the long-running operation are awaited synchronously, starting a long-running operation from a data view delegate is still strongly discouraged due to the following reasons:
- A synchronous wait until the data is loaded from the external service is bad for the user experience.
- The paging mechanisms of the Acumatica Framework may work incorrectly.
- The approach does not scale well. There are no observable problems when there are only 10-50 records retrieved from the external service. However, the solution rapidly deteriorates when the amount of external
data grows to something like 50 000 - 100 000. Such cases were observed by Acumatica support.
- Any disruption of the external service (for example, some trivial issues after OS upgrade on the server) will prevent the affected Acumatica screen from opening. Neither Acumatica, nor the customization authors
can guarantee the stability of the external service. Such situations take a lot of time and effort for investigation keeping the customer annoyed and frustrated with Acumatica even though the real problem was caused
by another service.

Long-running operations are started by the following APIs:
- `PXLongOperation.StartOperation` methods
Expand All @@ -17,6 +27,11 @@ Long-running operations are started by the following APIs:

To prevent the error from occurring, you should remove the code that starts a long-running operation from the data view delegate and rework the related business logic.

The alternative approach recommended by the Acumatica ISV Support team:
- If there is not much data to retrieve from the external service, then you can use a virtual DAC which is filled by clicking on an action.
- If there is a lot of data to retrieve from the external service, then you keep the data locally in the database. You should obtain the updates in the external service's data with a click on an action,
and synchronize them with the locally stored data. The synchronization of data can be also done via the push notifications mechanism.

## Example of Incorrect Code

```C#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ protected override Task RegisterCodeFixesForDiagnosticAsync(CodeFixContext conte
{
context.CancellationToken.ThrowIfCancellationRequested();

if (!diagnostic.IsRegisteredForCodeFix() ||
if (!diagnostic.IsRegisteredForCodeFix(considerRegisteredByDefault: false) ||
!diagnostic.TryGetPropertyValue(DiagnosticProperty.DacFieldName, out string? dacFieldName) ||
dacFieldName.IsNullOrWhiteSpace())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ private void AnalyzeDacPropertyDeclaredInThisType(SymbolAnalysisContext symbolCo
//
// The second scenario only makes sense for DACs, because DAC extensions can't have derived types,
// and it makes little sense to redeclare existing BQL field in the chained DAC extension.
if (declaredDacField.HasBqlFieldEffective && dacOrDacExt.DacType != DacType.Dac)
if (declaredDacField.HasBqlFieldEffective && dacOrDacExt.DacType == DacType.DacExtension)
return;

var declaredBqlFieldsWithTypo = FindBqlFieldInfosWithinTypoDistance(declaredDacField.Name, dacOrDacExt, declaredBqlFieldsWithWithoutProperty,
Expand All @@ -100,20 +100,20 @@ private void AnalyzeDacPropertyDeclaredInThisType(SymbolAnalysisContext symbolCo
private void AnalyzeDacPropertyDeclaredInBaseTypes(SymbolAnalysisContext symbolContext, PXContext pxContext, DacSemanticModel dacOrDacExt,
DacFieldInfo dacPropertyInBaseTypes, List<DacBqlFieldInfo> declaredBqlFieldsWithWithoutProperty)
{
// If the property is declared in based types and does not have a BQL field then report DAC field with PX1065 as missing BQL field
if (!dacPropertyInBaseTypes.HasBqlFieldEffective)
ReportDacPropertyWithoutBqlField(symbolContext, pxContext, dacOrDacExt, dacPropertyInBaseTypes);
// If the property is declared in base types and does not have a corresponding BQL field at all then
// Acuminator should not report the PX1065 diagnostic for the missing BQL field because there will be a lot of noise in DAC extensions and derived DACs.
// Even if the base DAC is located in the source code, it's unlikely that the extension authors will be able to fix the base DAC.

// Now check declared BQL fields without corresponding properties for ones with names close to the "dacPropertyInBaseTypes" name.
// Now check BQL fields declared in this DAC without a corresponding property for ones with names close to the "dacPropertyInBaseTypes" name.
//
// Since the DAC field "dacPropertyInBaseTypes" is declared in dacOrDacExt base types we need to support a scenario
// when BQL field with a typo is declared in the dacOrDacExt DAC and there is a DAC field with correct name in the base types.
// when BQL field with a typo is declared in the dacOrDacExt DAC under the validation and there is a property with correct name in the base types.
// It does not matter if that field has a BQL field or not - it is in base types, and checked bql fields are in derived types,
// so they would be hiding base BQL field anyway.
//
// This scenario only makes sense for DACs, because DAC extensions can't have derived types,
// and it makes little sense to redeclare existing BQL field in the chained DAC extension
if (dacOrDacExt.DacType != DacType.Dac)
if (dacOrDacExt.DacType == DacType.DacExtension)
return;

var declaredBqlFieldsWithTypo = FindBqlFieldInfosWithinTypoDistance(dacPropertyInBaseTypes.Name, dacOrDacExt, declaredBqlFieldsWithWithoutProperty,
Expand Down Expand Up @@ -184,21 +184,19 @@ private void ReportDacPropertyWithoutBqlField(SymbolAnalysisContext symbolContex
if (location == null)
return;

var properties = ImmutableDictionary<string, string?>.Empty;
string? propertyTypeName = dacFieldWithoutBqlField.PropertyTypeUnwrappedNullable?.GetSimplifiedName();
var properties = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
{ DiagnosticProperty.RegisterCodeFix, registerCodeFix.ToString() },
{ DiagnosticProperty.DacFieldName, dacFieldWithoutBqlField.Name }
};

if (registerCodeFix && propertyTypeName != null)
{
properties = new Dictionary<string, string?>
{
{ DiagnosticProperty.RegisterCodeFix, bool.TrueString },
{ DiagnosticProperty.DacFieldName, dacFieldWithoutBqlField.Name },
{ DiagnosticProperty.PropertyType, propertyTypeName }
}
.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase);
}
properties.Add(DiagnosticProperty.PropertyType, propertyTypeName);

var diagnostic = Diagnostic.Create(Descriptors.PX1065_NoBqlFieldForDacFieldProperty, location, properties, dacFieldWithoutBqlField.Name);
var diagnostic = Diagnostic.Create(Descriptors.PX1065_NoBqlFieldForDacFieldProperty, location,
properties.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase),
dacFieldWithoutBqlField.Name);
symbolContext.ReportDiagnosticWithSuppressionCheck(diagnostic, pxContext.CodeAnalysisSettings);
}

Expand All @@ -207,10 +205,9 @@ private void ReportDacPropertyWithoutBqlField(SymbolAnalysisContext symbolContex
if (dacField.PropertyInfo?.IsInSource == true && dacField.IsDeclaredInType(dac.Symbol))
{
var location = dacField.PropertyInfo.Node.Identifier.GetLocation().NullIfLocationKindIsNone() ??
dacField.PropertyInfo.Node.GetLocation() ??
dac.Node!.Identifier.GetLocation().NullIfLocationKindIsNone();
dacField.PropertyInfo.Node.GetLocation();

return (location, RegisterCodeFix: true);
return (location, RegisterCodeFix: location != null);
}

// Node is not null because aggregated DAC analysis runs only on DACs from the source code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,15 @@ public abstract class status : PX.Data.BQL.BqlString.Field<status> { }
public virtual int? ShipmentNbr { get; set; }

public virtual string? ExtraData { get; set; }

#region Amount
[PXDBDecimal]
[PXUIField(DisplayName = "Amount")]
public decimal? Amount // PX1065 should be reported here because the property does not have a BQL field
{
get;
set;
}
#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,28 @@ public async Task RegularDac_WithoutBqlFields(string actual) => await VerifyCSha
[Theory]
[EmbeddedFileData(@"MissingBqlField\DacWithBqlFieldMissingInBaseDac.cs")]
public async Task DerivedDac_BasedDac_WithoutBqlFields(string actual) => await VerifyCSharpDiagnosticAsync(actual,
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(8, 15, "ShipmentNbr"),
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(23, 23, "ShipmentNbr"));
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(19, 28, "Amount"),
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(36, 23, "ShipmentNbr"),
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(43, 27, "Amount"));

[Theory]
[EmbeddedFileData(@"MissingBqlField\DacExtensionWithBqlFieldMissingInBaseDac.cs")]
public async Task DacExtension_BasedDac_WithoutBqlFields(string actual) => await VerifyCSharpDiagnosticAsync(actual,
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(9, 22, "ShipmentNbr"),
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(18, 16, "Selected"),
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(34, 23, "ShipmentNbr"));
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(20, 16, "Selected"),
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(30, 19, "Amount"),
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(47, 23, "ShipmentNbr"),
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(54, 19, "Amount"));

[Theory]
[EmbeddedFileData(@"MissingBqlField\DacWithBqlFieldMissingInExternalBaseDac.cs")]
public async Task DerivedDac_BasedDacInExternalDll_WithoutBqlFields(string actual) => await VerifyCSharpDiagnosticAsync(actual,
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(8, 15, "ShipmentNbr"),
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(12, 25, "OrderNbr"));
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(13, 25, "OrderNbr"),
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(22, 19, "Amount"));

[Theory]
[EmbeddedFileData(@"MissingBqlField\DacExtensionWithBqlFieldMissingInExternalBaseDac.cs")]
public async Task DacExtension_BasedDacInExternalDll_WithoutBqlFields(string actual) => await VerifyCSharpDiagnosticAsync(actual,
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(11, 22, "ShipmentNbr"));
Descriptors.PX1065_NoBqlFieldForDacFieldProperty.CreateFor(21, 19, "Amount"));

[Theory]
[EmbeddedFileData(@"MissingBqlField\DacWithoutBqlFields.cs", @"MissingBqlField\DacWithoutBqlFields_Expected.cs")]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
using System;

using PX.Data;
using PX.Data.BQL;

namespace PX.Analyzers.Test.Sources
{
/// <exclude/>
// Acuminator disable once PX1016 ExtensionDoesNotDeclareIsActiveMethod extension should be constantly active
[PXHidden]
public sealed class DacExtension : PXCacheExtension<BaseDac>
public sealed class DacExtension : PXCacheExtension<BaseDac> // PX1065 for ShipmentNbr should not be reported here because it is not declared in the extension
{
[PXString]
[PXUIField(DisplayName = "Status")]
public string Status { get; set; }
public string? Status { get; set; }

#region Selected
[PXBool]
Expand All @@ -21,8 +23,19 @@ public bool? Selected
set;
}
#endregion

#region Amount
[PXDBDecimal]
[PXUIField(DisplayName = "Customized Amount")]
public decimal? Amount // PX1065 should be reported here because the base property without a BQL field is declared in the extension
{
get;
set;
}
#endregion
}

// Acuminator disable once PX1069 MissingMandatoryDacFields [Justification]
[PXHidden]
public class BaseDac : PXBqlTable, IBqlTable
{
Expand All @@ -33,6 +46,16 @@ public abstract class status : PX.Data.BQL.BqlString.Field<status> { }
[PXInt]
public virtual int? ShipmentNbr { get; set; }

public virtual string ExtraData { get; set; }
public virtual string? ExtraData { get; set; }

#region Amount
[PXDBDecimal]
[PXUIField(DisplayName = "Amount")]
public decimal? Amount // PX1065 should be reported here because the property does not have a BQL field
{
get;
set;
}
#endregion
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
using System;

using PX.Data;
using PX.Data.BQL;

namespace PX.Analyzers.Test.Sources
{
/// <exclude/>
// Acuminator disable once PX1016 ExtensionDoesNotDeclareIsActiveMethod extension should be constantly active
[PXHidden]
public sealed class DacExtension : PXCacheExtension<BaseDac>
public sealed class DacExtension : PXCacheExtension<BaseDac> // PX1065 for ShipmentNbr should not be reported here because it is not declared in the extension
{
[PXString]
[PXUIField(DisplayName = "Status")]
public string Status { get; set; }
public string? Status { get; set; }
#region Selected
public abstract class selected : PX.Data.BQL.BqlBool.Field<selected> { }

Expand All @@ -22,8 +24,20 @@ public bool? Selected
set;
}
#endregion
#region Amount
public abstract class amount : PX.Data.BQL.BqlDecimal.Field<amount> { }

[PXDBDecimal]
[PXUIField(DisplayName = "Customized Amount")]
public decimal? Amount // PX1065 should be reported here because the base property without a BQL field is declared in the extension
{
get;
set;
}
#endregion
}

// Acuminator disable once PX1069 MissingMandatoryDacFields [Justification]
[PXHidden]
public class BaseDac : PXBqlTable, IBqlTable
{
Expand All @@ -35,6 +49,17 @@ public abstract class shipmentNbr : PX.Data.BQL.BqlInt.Field<shipmentNbr> { }
[PXInt]
public virtual int? ShipmentNbr { get; set; }

public virtual string ExtraData { get; set; }
public virtual string? ExtraData { get; set; }
#region Amount
public abstract class amount : PX.Data.BQL.BqlDecimal.Field<amount> { }

[PXDBDecimal]
[PXUIField(DisplayName = "Amount")]
public decimal? Amount // PX1065 should be reported here because the property does not have a BQL field
{
get;
set;
}
#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,23 @@

namespace PX.Analyzers.Test.Sources
{
/// <exclude/>
// Acuminator disable once PX1016 ExtensionDoesNotDeclareIsActiveMethod extension should be constantly active
[PXHidden]
public sealed class DacExtensionOnExternalDac : PXCacheExtension<BaseDacWithoutBqlField>
public sealed class DacExtensionOnExternalDac : PXCacheExtension<BaseDacWithoutBqlField> // No PX1065 should be reported here because ShipmentNbr is not declared in the extension
{
[PXString]
[PXUIField(DisplayName = "Status")]
public string Status { get; set; }
public string? Status { get; set; }

#region Amount
[PXDBDecimal]
[PXUIField(DisplayName = "Customized Amount")]
public decimal? Amount // PX1065 should be reported here because the base property without a BQL field is declared in the extension
{
get;
set;
}
#endregion
}
}
Loading