Skip to content

Commit

Permalink
Migrate value truncation check to an Analyzer (#398)
Browse files Browse the repository at this point in the history
closes #347
  • Loading branch information
MarkusAmshove authored Oct 9, 2023
1 parent 03c65fa commit d22575b
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.amshove.natlint.analyzers;

import org.amshove.natlint.api.AbstractAnalyzer;
import org.amshove.natlint.api.DiagnosticDescription;
import org.amshove.natlint.api.IAnalyzeContext;
import org.amshove.natlint.api.ILinterContext;
import org.amshove.natparse.DiagnosticSeverity;
import org.amshove.natparse.ReadOnlyList;
import org.amshove.natparse.natural.*;

public class ValueTruncationAnalyzer extends AbstractAnalyzer
{
public static final DiagnosticDescription VALUE_TRUNCATED = DiagnosticDescription.create(
"NL021",
"Value is truncated from %s to %s at runtime. Extend the target variable or remove the truncated parts from this literal.",
DiagnosticSeverity.WARNING
);

@Override
public ReadOnlyList<DiagnosticDescription> getDiagnosticDescriptions()
{
return ReadOnlyList.of(VALUE_TRUNCATED);
}

@Override
public void initialize(ILinterContext context)
{
context.registerNodeAnalyzer(IAssignmentStatementNode.class, this::analyzeAssign);
context.registerNodeAnalyzer(ITypedVariableNode.class, this::analyzeInitialValue);
}

private void analyzeInitialValue(ISyntaxNode typedVariable, IAnalyzeContext context)
{
var typedVar = (ITypedVariableNode) typedVariable;
var initialNode = typedVar.type().initialValue();
if (initialNode == null || typedVar.type().hasDynamicLength() || !(initialNode instanceof ILiteralNode || initialNode instanceof IStringConcatOperandNode))
{
return;
}

var inferredInitialType = initialNode instanceof ILiteralNode literal
? literal.reInferType(typedVar.type())
: ((IStringConcatOperandNode) initialNode).inferType();

checkTruncation(inferredInitialType, typedVar.type(), initialNode, context);
}

private void analyzeAssign(ISyntaxNode assignNode, IAnalyzeContext context)
{
var assignment = (IAssignmentStatementNode) assignNode;

if (!(assignment.target()instanceof IVariableReferenceNode targetRef
&& targetRef.reference()instanceof ITypedVariableNode typedTarget
&& typedTarget.type() != null))
{
return;
}

if (assignment.operand()instanceof ILiteralNode literal)
{
checkTruncation(literal.reInferType(typedTarget.type()), typedTarget.type(), assignment.operand(), context);
}
}

private void checkTruncation(IDataType operandType, IDataType targetType, ISyntaxNode location, IAnalyzeContext context)
{
if (!operandType.hasCompatibleFormat(targetType))
{
return;
}

if (!operandType.fitsInto(targetType))
{
context.report(
VALUE_TRUNCATED.createFormattedDiagnostic(
location.diagnosticPosition(),
operandType.toShortString(),
targetType.toShortString()
)
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.amshove.natlint.analyzers;

import org.amshove.natlint.linter.AbstractAnalyzerTest;
import org.junit.jupiter.api.Test;

class ValueTruncationAnalyzerShould extends AbstractAnalyzerTest
{
@Test
void raiseADiagnosticWhenAConstInitializerIsTruncatedForCompatibleFormats()
{
testDiagnostics("""
DEFINE DATA LOCAL
1 #C-CONST (A1) CONST<20>
END-DEFINE
""", expectDiagnostic(1, ValueTruncationAnalyzer.VALUE_TRUNCATED));
}

@Test
void raiseADiagnosticWhenAnInitInitializerIsTruncatedForCompatibleFormats()
{
testDiagnostics("""
DEFINE DATA LOCAL
1 #C-CONST (A1) INIT<20>
END-DEFINE
""", expectDiagnostic(1, ValueTruncationAnalyzer.VALUE_TRUNCATED));
}

@Test
void reportADiagnosticWhenNumericAssignmentValuesGetTruncated()
{
testDiagnostics(
"""
DEFINE DATA LOCAL
1 #CONST-N1-I4 (I4) CONST<1>
1 #N1 (N1)
1 #I1 (I1)
END-DEFINE
#N1 := 23
#I1 := 128
END
""",
expectDiagnostic(5, ValueTruncationAnalyzer.VALUE_TRUNCATED),
expectDiagnostic(6, ValueTruncationAnalyzer.VALUE_TRUNCATED)
);
}

@Test
void reportADiagnosticWhenNumericAssignmentValuesGetTruncatedWithCompatibleTargetFormat()
{
testDiagnostics("""
DEFINE DATA LOCAL
1 #CONST-N1-I4 (I4) CONST<1>
1 #A1 (A1)
END-DEFINE
#A1 := 10
END
""", expectDiagnostic(4, ValueTruncationAnalyzer.VALUE_TRUNCATED));
}

@Test
void reportADiagnosticWhenAlphanumericAssignmentValuesGetTruncated()
{
testDiagnostics("""
DEFINE DATA LOCAL
1 #A1 (A1)
END-DEFINE
#A1 := 'AB'
END
""", expectDiagnostic(3, ValueTruncationAnalyzer.VALUE_TRUNCATED));
}

protected ValueTruncationAnalyzerShould()
{
super(new ValueTruncationAnalyzer());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,11 @@ public enum ParserError
INVALID_ARRAY_ACCESS("NPP042"),
STATEMENT_HAS_EMPTY_BODY("NPP043"),
DECIDE_MISSES_NONE_BRANCH("NPP044"),
LITERAL_VALUE_TRUNCATED("NPP045"),
EMHDPM_NOT_ALLOWED_IN_SCOPE("NPP045"),
GROUP_HAS_MIXED_CONST("NPP046"),
CYCLOMATIC_INCLUDE("NPP047"),
UNEXPECTED_TOKEN_EXPECTED_IDENTIFIER("NPP048"),
UNEXPECTED_TOKEN_EXPECTED_OPERAND("NPP049"),
EMHDPM_NOT_ALLOWED_IN_SCOPE("NPP050"),
;

private final String id;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.amshove.natparse.parsing;

import org.amshove.natparse.AdditionalDiagnosticInfo;
import org.amshove.natparse.DiagnosticSeverity;
import org.amshove.natparse.IDiagnostic;
import org.amshove.natparse.lexing.SyntaxKind;
import org.amshove.natparse.lexing.SyntaxToken;
Expand Down Expand Up @@ -556,16 +555,6 @@ public static IDiagnostic referenceNotMutable(String message, ISyntaxNode node)
);
}

public static IDiagnostic valueTruncation(String message, ISyntaxNode node)
{
return ParserDiagnostic.create(
message,
node,
ParserError.LITERAL_VALUE_TRUNCATED,
DiagnosticSeverity.WARNING
);
}

public static IDiagnostic typeMismatch(String message, ISyntaxNode node)
{
return ParserDiagnostic.create(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ private void checkAssign(IAssignmentStatementNode assignment)
// Only do this for literals
// #N5 := #N10 is legal compiler wise, but might result in a runtime error
{
checkTypeCompatibleOrTruncation(operandType, targetType, assignment.operand());
checkTypeCompatible(operandType, targetType, assignment.operand());
return;
}

Expand All @@ -184,26 +184,14 @@ private void checkTypeConvertable(IDataType operandType, IDataType targetType, I
}
}

private void checkTypeCompatibleOrTruncation(IDataType operandType, IDataType targetType, ISyntaxNode location)
private void checkTypeCompatible(IDataType operandType, IDataType targetType, ISyntaxNode location)
{
if (operandType.fitsInto(targetType))
{
return;
}

if (operandType.hasCompatibleFormat(targetType))
{
report(
ParserErrors.valueTruncation(
"Value is truncated from %s to %s at runtime. Extend the target variable or remove the truncated parts from this literal.".formatted(
operandType.toShortString(),
targetType.toShortString()
),
location
)
);
}
else
if (!operandType.hasCompatibleFormat(targetType))
{
report(
ParserErrors.typeMismatch(
Expand Down Expand Up @@ -521,7 +509,7 @@ private void checkVariableInitType(ITypedVariableNode typedVariable)
}
else
{
checkTypeCompatibleOrTruncation(inferredInitialType, typedVariable.type(), initialNode);
checkTypeCompatible(inferredInitialType, typedVariable.type(), initialNode);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ LOCAL
1 #C-A-CONST-1 (A1) CONST<'ABC'> /* !{D:ERROR:NPP037}
1 #C-A-CONST-2 (A1) CONST<'A'> /* Length matches
1 #C-A-CONST-3 (A5) CONST<'A'> /* Length is smaller
1 #C-A-CONST-NUM (A1) CONST<20> /* !{D:WARNING:NPP045} value is truncated

1 #C-I-1 (I1) INIT<-1> /* Length matches
1 #C-I-1-BIG (I1) INIT<280> /* !{D:ERROR:NPP037}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ DEFINE DATA LOCAL
END-DEFINE

/* NPP037: Type mismatch
/* NPP045: Literal value truncation

/* Format N
#N1 := #N1
Expand Down Expand Up @@ -260,9 +259,7 @@ END-DEFINE
#F := *ISN
#F := *PROGRAM /* !{D:ERROR:NPP037}

#N1 := 23 /* !{D:WARNING:NPP045}
#N1 := 2
#I1 := 128 /* !{D:WARNING:NPP045}
#I1 := 127

#N1 := #CONST-N1-I4 /* Should work, the constant *value* is N1
Expand All @@ -273,7 +270,6 @@ END-DEFINE
#A1 := *OCC(#ARR) /* I4 has implicit conversion to A1
#A1 := #N1 /* N1 has implicit conversion to A1
#A1 := 1 /* I1 has implicit conversion to A1
#A1 := 10 /* !{D:WARNING:NPP045} value is truncated

#A10 := #T /* Implicit conversion
#A10 := #D /* Implicit conversion
Expand Down
30 changes: 30 additions & 0 deletions tools/ruletranslator/src/main/resources/rules/NL021
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Literal value truncated at runtime
priority: MINOR
tags: pitfall,confusing
type: CODE_SMELL
description:
The value of this literal is truncated at runtime. This will raise unexpected results.

== Non compliant

``
DEFINE DATA LOCAL
1 #VAR (A1)
END-DEFINE

#VAR := 'Hi'
WRITE #VAR
END
``

== Compliant

``
DEFINE DATA LOCAL
1 #VAR (A1)
END-DEFINE

#VAR := 'H'
WRITE #VAR
END
``
34 changes: 5 additions & 29 deletions tools/ruletranslator/src/main/resources/rules/NPP045
Original file line number Diff line number Diff line change
@@ -1,30 +1,6 @@
name: Literal value truncated at runtime
priority: MINOR
tags: pitfall,confusing
type: CODE_SMELL
name: EM, HD, PM not allowed in this scope
priority: BLOCKER
tags: natparse-internal,compile-time
type: BUG
description:
The value of this literal is truncated at runtime. This will raise unexpected results.

== Non compliant

``
DEFINE DATA LOCAL
1 #VAR (A1)
END-DEFINE

#VAR := 'Hi'
WRITE #VAR
END
``

== Compliant

``
DEFINE DATA LOCAL
1 #VAR (A1)
END-DEFINE

#VAR := 'H'
WRITE #VAR
END
``
``EM, HD, PM`` is not allowed in ``PARAMETER`` scope.
6 changes: 0 additions & 6 deletions tools/ruletranslator/src/main/resources/rules/NPP050

This file was deleted.

0 comments on commit d22575b

Please sign in to comment.