Skip to content
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Support for MSBuild item and item metadata expressions in project files.
- Debug logging for name resolution failures (enabled with `sonar-scanner -X`).
- `ExhaustiveEnumCase` analysis rule, which flags `case` statements that do not handle all values in an enumeration.
- `IterationPastHighBound` analysis rule, which flags `for` loops that iterate past the end of the collection.
- `ExplicitBitwiseNot` analysis rule, which flags potentially incorrect bitwise `not` operations.
Expand All @@ -36,7 +37,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Detect tab-indented multiline strings in `TabulationCharacter`.
- Improve support for evaluating name references in compiler directive expressions.
- Improve overload resolution in cases involving generic type parameter constraints.
- Improve handling for MSBuild properties, items, and conditional evaluation.
- Improve handling for MSBuild properties, items, and conditional evaluation.
- Perform best-effort name resolution of arguments to unresolved routines or array properties, which
improves analysis quality in cases where symbol information is incomplete.

### Deprecated

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -557,9 +557,54 @@ private Path createStandardLibrary() {
+ "\n"
+ "interface\n"
+ "\n"
+ "const\n"
+ " vtInteger = 0;\n"
+ " vtBoolean = 1;\n"
+ " vtChar = 2;\n"
+ " vtExtended = 3;\n"
+ " vtString = 4;\n"
+ " vtPointer = 5;\n"
+ " vtPChar = 6;\n"
+ " vtObject = 7;\n"
+ " vtClass = 8;\n"
+ " vtWideChar = 9;\n"
+ " vtPWideChar = 10;\n"
+ " vtAnsiString = 11;\n"
+ " vtCurrency = 12;\n"
+ " vtVariant = 13;\n"
+ " vtInterface = 14;\n"
+ " vtWideString = 15;\n"
+ " vtInt64 = 16;\n"
+ " vtUnicodeString = 17;\n"
+ "\n"
+ "type\n"
+ " Int8 = ShortInt;\n"
+ " Int16 = SmallInt;\n"
+ " Int32 = Integer;\n"
+ " IntPtr = NativeInt;\n"
+ " UInt8 = Byte;\n"
+ " UInt16 = Word;\n"
+ " UInt32 = Cardinal;\n"
+ " UIntPtr = NativeUInt;\n"
+ " Float32 = Single;\n"
+ " Float64 = Double;\n"
+ "\n"
+ " HRESULT = type Int32;\n"
+ "\n"
+ " TArray<T> = array of T;\n"
+ "\n"
+ " PGUID = ^TGUID;\n"
+ " TGUID = record\n"
+ " end;\n"
+ "\n"
+ " PInterfaceEntry = ^TInterfaceEntry;\n"
+ " TInterfaceEntry = packed record\n"
+ " end;\n"
+ "\n"
+ " PInterfaceTable = ^TInterfaceTable;\n"
+ " TInterfaceTable = packed record\n"
+ " end;\n"
+ "\n"
+ " TObject = class;\n"
+ "\n"
+ " TClass = class of TObject;\n"
Expand Down Expand Up @@ -589,11 +634,11 @@ private Path createStandardLibrary() {
+ " class function ClassInfo: Pointer; inline;\n"
+ " class function InstanceSize: Longint; inline;\n"
+ " class function InheritsFrom(AClass: TClass): Boolean;\n"
+ " class function MethodAddress(const Name: _ShortStr): Pointer; overload;\n"
+ " class function MethodAddress(const Name: ShortString): Pointer; overload;\n"
+ " class function MethodAddress(const Name: string): Pointer; overload;\n"
+ " class function MethodName(Address: Pointer): string;\n"
+ " class function QualifiedClassName: string;\n"
+ " function FieldAddress(const Name: _ShortStr): Pointer; overload;\n"
+ " function FieldAddress(const Name: ShortString): Pointer; overload;\n"
+ " function FieldAddress(const Name: string): Pointer; overload;\n"
+ " function GetInterface(const IID: TGUID; out Obj): Boolean;\n"
+ " class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;\n"
Expand All @@ -620,6 +665,8 @@ private Path createStandardLibrary() {
+ " function _Release: Integer; stdcall;\n"
+ " end;\n"
+ "\n"
+ " IUnknown = IInterface;\n"
+ "\n"
+ " IEnumerator = interface(IInterface)\n"
+ " function GetCurrent: TObject;\n"
+ " function MoveNext: Boolean;\n"
Expand Down Expand Up @@ -652,17 +699,54 @@ private Path createStandardLibrary() {
+ " function Equals(Value: T): Boolean;\n"
+ " end;\n"
+ "\n"
+ " PLongInt = ^LongInt;\n"
+ " PInteger = ^Integer;\n"
+ " PCardinal = ^Cardinal;\n"
+ " PWord = ^Word;\n"
+ " PSmallInt = ^SmallInt;\n"
+ " {$POINTERMATH ON}\n"
+ " PByte = ^Byte;\n"
+ " {$POINTERMATH OFF}\n"
+ " PShortInt = ^ShortInt;\n"
+ " PUint32 = ^Uint32;\n"
+ " PInt64 = ^Int64;\n"
+ " PUInt64 = ^UInt64;\n"
+ " PLongWord = ^LongWord;\n"
+ " PSingle = ^Single;\n"
+ " PDouble = ^Double;\n"
+ " PDate = ^Double;\n"
+ " PWordBool = ^WordBool;\n"
+ " PUnknown = ^IUnknown;\n"
+ " PPUnknown = ^PUnknown;\n"
+ " PInterface = ^IInterface;\n"
+ " PPWideChar = ^PWideChar;\n"
+ " PPChar = PPWideChar;\n"
+ " PExtended = ^Extended;\n"
+ " PComp = ^Comp;\n"
+ " PCurrency = ^Currency;\n"
+ " PVariant = ^Variant;\n"
+ " POleVariant = ^OleVariant;\n"
+ " PPointer = ^Pointer;\n"
+ " PBoolean = ^Boolean;\n"
+ " PNativeInt = ^NativeInt;\n"
+ " PNativeUInt = ^NativeUInt;\n"
+ " PShortString = ^ShortString;\n"
+ " PAnsiString = ^AnsiString;\n"
+ " PWideString = ^WideString;\n"
+ " PUnicodeString = ^UnicodeString;\n"
+ " PString = PUnicodeString;\n"
+ "\n"
+ " PVarRec = ^TVarRec;\n"
+ " TVarRec = record\n"
+ " case Integer of\n"
+ " 0: (case Byte of\n"
+ " vtInteger: (VInteger: Integer);\n"
+ " vtBoolean: (VBoolean: Boolean);\n"
+ " vtChar: (VChar: _AnsiChr);\n"
+ " vtChar: (VChar: AnsiChar);\n"
+ " vtExtended: (VExtended: PExtended);\n"
+ " vtString: (VString: _PShortStr);\n"
+ " vtString: (VString: PShortString);\n"
+ " vtPointer: (VPointer: Pointer);\n"
+ " vtPChar: (VPChar: _PAnsiChr);\n"
+ " vtPChar: (VPChar: PAnsiChar);\n"
+ " vtObject: (VObject: TObject);\n"
+ " vtClass: (VClass: TClass);\n"
+ " vtWideChar: (VWideChar: WideChar);\n"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ private void handleNestedRoutineInvocations(StatementNode statement, DelphiCheck

private void searchForInitializationsByOutArgument(StatementNode statement) {
findNameReferences(statement).stream()
.filter(VariableInitializationCheck::isOutArgument)
.filter(VariableInitializationCheck::isPotentiallyInitializingArgument)
.map(this::getReferredInitializationState)
.filter(Objects::nonNull)
.forEach(InitializationState::assignedTo);
Expand Down Expand Up @@ -275,7 +275,7 @@ private void handleAssignment(StatementNode statement) {
}

for (NameReferenceNode name : findNameReferences(assignmentStatement.getValue())) {
if (name.getNameDeclaration() == declaration && !isOutArgument(name)) {
if (name.getNameDeclaration() == declaration && !isPotentiallyInitializingArgument(name)) {
return;
}
}
Expand All @@ -294,7 +294,7 @@ private void handleAssignment(StatementNode statement) {
}
}

private static boolean isOutArgument(NameReferenceNode name) {
private static boolean isPotentiallyInitializingArgument(NameReferenceNode name) {
ArgumentNode argument = getArgument(name);
if (argument == null) {
return false;
Expand All @@ -303,7 +303,10 @@ private static boolean isOutArgument(NameReferenceNode name) {
ArgumentListNode argumentList = (ArgumentListNode) argument.getParent();
ProceduralType procedural = getInvokedProcedural(argumentList);
if (procedural == null) {
return false;
// If we can't find the procedural, it might mean:
// - an unresolved procedural reference (potentially initializing)
// - an explicit array constructor (not initializing)
return !isExplicitArrayConstructor(argumentList);
}

int argumentIndex = argumentList.getArgumentNodes().indexOf(argument);
Expand Down Expand Up @@ -400,6 +403,22 @@ private static ProceduralType getInvokedProcedural(ArgumentListNode argumentList
return (ProceduralType) type;
}

private static boolean isExplicitArrayConstructor(ArgumentListNode argumentList) {
Node previous = argumentList.getParent().getChild(argumentList.getChildIndex() - 1);
if (previous instanceof NameReferenceNode) {
NameReferenceNode name = ((NameReferenceNode) previous).getLastName();
NameReferenceNode prevName = name.prevName();
if (prevName != null) {
NameDeclaration arrayDeclaration = prevName.getNameDeclaration();
if (arrayDeclaration instanceof TypeNameDeclaration) {
return ((TypeNameDeclaration) arrayDeclaration).getType().isDynamicArray()
&& name.simpleName().equalsIgnoreCase("Create");
}
}
}
return false;
}

private static RoutineNameDeclaration getRoutineDeclaration(ArgumentListNode argumentList) {
Node previous = argumentList.getParent().getChild(argumentList.getChildIndex() - 1);
if (!(previous instanceof NameReferenceNode)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,22 +720,22 @@ void testPassedToProcVarOutParameterShouldNotAddIssue() {
}

@Test
void testFailOnUpgradeVarPassedToArrayProcVarOutParameterShouldAddIssue() {
void testFailOnUpgradePassedToArrayElementProceduralShouldNotAddIssue() {
CheckVerifier.newVerifier()
.withCheck(new VariableInitializationCheck())
.onFile(
new DelphiTestUnitBuilder()
.appendDecl("type")
.appendDecl(" TTestProc = reference to procedure(out Int: Integer);")
.appendDecl(" TTestProc = reference to procedure(Int: Integer);")
.appendDecl("procedure Foo(Int: Integer);")
.appendImpl("procedure Test(ProcArray: array of TTestProc);")
.appendImpl("var")
.appendImpl(" I: Integer;")
.appendImpl("begin")
.appendImpl(" ProcArray[0](I); // Noncompliant")
.appendImpl(" ProcArray[0](I);")
.appendImpl(" Foo(I);")
.appendImpl("end;"))
.verifyIssues();
.verifyNoIssues();
}

@Test
Expand Down Expand Up @@ -947,6 +947,24 @@ void testUnknownRoutineArgumentShouldNotAddIssue() {
.verifyNoIssues();
}

@Test
void testUninitializedArgumentToExplicitArrayConstructorShouldAddIssue() {
CheckVerifier.newVerifier()
.withCheck(new VariableInitializationCheck())
.onFile(
new DelphiTestUnitBuilder()
.appendImpl("procedure Test;")
.appendImpl("type")
.appendImpl(" TBar = array of Integer;")
.appendImpl("var")
.appendImpl(" Foo: Integer;")
.appendImpl(" Bar: TBar;")
.appendImpl("begin")
.appendImpl(" Bar := TBar.Create(Foo); // Noncompliant")
.appendImpl("end;"))
.verifyIssues();
}

@Test
void testUninitializedObjectWithFreeAndNilShouldAddIssue() {
CheckVerifier.newVerifier()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,11 @@
import org.sonar.plugins.communitydelphi.api.ast.PointerTypeNode;
import org.sonar.plugins.communitydelphi.api.ast.PrimaryExpressionNode;
import org.sonar.plugins.communitydelphi.api.ast.ProcedureTypeHeadingNode;
import org.sonar.plugins.communitydelphi.api.ast.PropertyImplementsSpecifierNode;
import org.sonar.plugins.communitydelphi.api.ast.PropertyNode;
import org.sonar.plugins.communitydelphi.api.ast.PropertyReadSpecifierNode;
import org.sonar.plugins.communitydelphi.api.ast.PropertyStoredSpecifierNode;
import org.sonar.plugins.communitydelphi.api.ast.PropertyWriteSpecifierNode;
import org.sonar.plugins.communitydelphi.api.ast.RecordTypeNode;
import org.sonar.plugins.communitydelphi.api.ast.RecordVariantTagNode;
import org.sonar.plugins.communitydelphi.api.ast.RepeatStatementNode;
Expand Down Expand Up @@ -517,6 +521,30 @@ public Data visit(PropertyNode node, Data data) {
return createDeclarationScope(node, data);
}

@Override
public Data visit(PropertyReadSpecifierNode node, Data data) {
// Already resolved by the PropertyNode visit.
return data;
}

@Override
public Data visit(PropertyWriteSpecifierNode node, Data data) {
// Already resolved by the PropertyNode visit.
return data;
}

@Override
public Data visit(PropertyImplementsSpecifierNode node, Data data) {
// Already resolved by the PropertyNode visit.
return data;
}

@Override
public Data visit(PropertyStoredSpecifierNode node, Data data) {
// Already resolved by the PropertyNode visit.
return data;
}

@Nullable
private static PropertyNameDeclaration findConcretePropertyDeclaration(PropertyNode property) {
if (!property.getType().isUnknown()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.sonar.plugins.communitydelphi.api.ast.utils.ExpressionNodeUtils;
import org.sonar.plugins.communitydelphi.api.type.Type;
import org.sonar.plugins.communitydelphi.api.type.Type.ProceduralType;
import org.sonar.plugins.communitydelphi.api.type.TypeFactory;
import org.sonar.plugins.communitydelphi.api.type.Typed;

public class InvocationArgument implements Typed {
Expand All @@ -55,7 +56,7 @@ public class InvocationArgument implements Typed {
void resolve(Type parameterType) {
if (resolver != null) {
if (isRoutineReference(parameterType)) {
disambiguateRoutineReference(resolver, parameterType);
resolver.disambiguateRoutineReference((ProceduralType) parameterType);
} else if (!resolver.isExplicitInvocation()) {
resolver.disambiguateImplicitEmptyArgumentList();
}
Expand Down Expand Up @@ -92,13 +93,12 @@ Type findRoutineReferenceType(Type parameterType) {
Preconditions.checkNotNull(resolver);

NameResolver clone = new NameResolver(resolver);
disambiguateRoutineReference(clone, parameterType);
return clone.getApproximateType();
}
clone.disambiguateRoutineReference((ProceduralType) parameterType);
if (!clone.isAmbiguous()) {
return clone.getApproximateType();
}

private static void disambiguateRoutineReference(NameResolver resolver, Type parameterType) {
resolver.disambiguateRoutineReference((ProceduralType) parameterType);
resolver.checkAmbiguity();
return TypeFactory.unknownType();
}

@Override
Expand Down
Loading