Skip to content

Commit

Permalink
add new rule "Replace CONDENSE with string function" (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmgrassau committed Mar 25, 2024
1 parent 0cbc20e commit a4382b9
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import java.util.*;

public abstract class Rule {
public static final int RULE_COUNT = 74;
public static final int RULE_COUNT = 75;
public static final int RULE_GROUP_COUNT = 7;

protected static final String LINE_SEP = ABAP.LINE_SEPARATOR;
Expand Down Expand Up @@ -114,6 +114,7 @@ static Rule[] getAllRules(Profile profile) {
new AddToEtcRule(profile),
new MoveToRule(profile),
new TranslateRule(profile),
new CondenseRule(profile),
new DescribeTableRule(profile),
new ReadTableRule(profile),
new AssertEqualsBooleanRule(profile),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public enum RuleID {
ADD_TO_ETC,
MOVE_TO,
TRANSLATE,
CONDENSE,
DESCRIBE_TABLE,
READ_TABLE,
// - ASSERTs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
import java.util.*;

public class AlignParametersRule extends RuleForCommands {
public static final String DISPLAY_NAME = "Align parameters and components";
public static final String OPTION_NAME_KEEP_OTHER_ONE_LINERS = "Keep other one-liners";

public enum Columns {
// LET ... IN expressions
LET_KEYWORD,
Expand Down Expand Up @@ -70,7 +73,7 @@ private TableStart(int startIndent, boolean continueOnSameLine, boolean forceTab
public RuleGroupID getGroupID() { return RuleGroupID.ALIGNMENT; }

@Override
public String getDisplayName() { return "Align parameters and components"; }
public String getDisplayName() { return DISPLAY_NAME; }

@Override
public String getDescription() {
Expand Down Expand Up @@ -160,7 +163,7 @@ public String getExample() {
final ConfigBoolValue configAlignAcrossTableRows = new ConfigBoolValue(this, "AlignAcrossTableRows", "Align assignments across rows of table constructors", true, false, LocalDate.of(2023, 6, 9));
final ConfigEnumValue<ComponentsOnSingleLine> configKeepComponentsOnSingleLine = new ConfigEnumValue<ComponentsOnSingleLine>(this, "KeepParametersOnSingleLine", "Table rows: Keep multiple components on single line",
new String[] { "never", "if maximum line length B is observed", "always" }, ComponentsOnSingleLine.values(), ComponentsOnSingleLine.IF_BELOW_MAX_LINE_LENGTH);
final ConfigEnumValue<ComponentsOnSingleLine> configKeepOtherOneLiners = new ConfigEnumValue<ComponentsOnSingleLine>(this, "KeepOtherOneLiners", "Keep other one-liners",
final ConfigEnumValue<ComponentsOnSingleLine> configKeepOtherOneLiners = new ConfigEnumValue<ComponentsOnSingleLine>(this, "KeepOtherOneLiners", OPTION_NAME_KEEP_OTHER_ONE_LINERS,
new String[] { "never", "if maximum line length A is observed", "always" }, ComponentsOnSingleLine.values(), ComponentsOnSingleLine.NEVER, ComponentsOnSingleLine.NEVER, LocalDate.of(2024, 1, 1));
final ConfigEnumValue<ContentLeftOfAssignOp> configAllowContentLeftOfAssignOp = new ConfigEnumValue<ContentLeftOfAssignOp>(this, "AllowContentLeftOfAssignOp", "Allow line starts left of assignment operator",
new String[] { "never", "only to keep maximum line length", "always" }, ContentLeftOfAssignOp.values(), ContentLeftOfAssignOp.TO_KEEP_MAX_LINE_LENGTH, ContentLeftOfAssignOp.NEVER, LocalDate.of(2022, 3, 19));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package com.sap.adt.abapcleaner.rules.commands;

import java.time.LocalDate;

import com.sap.adt.abapcleaner.parser.Code;
import com.sap.adt.abapcleaner.parser.Command;
import com.sap.adt.abapcleaner.parser.Token;
import com.sap.adt.abapcleaner.parser.TokenSearch;
import com.sap.adt.abapcleaner.programbase.IntegrityBrokenException;
import com.sap.adt.abapcleaner.programbase.UnexpectedSyntaxAfterChanges;
import com.sap.adt.abapcleaner.programbase.UnexpectedSyntaxBeforeChanges;
import com.sap.adt.abapcleaner.rulebase.ConfigBoolValue;
import com.sap.adt.abapcleaner.rulebase.ConfigValue;
import com.sap.adt.abapcleaner.rulebase.Profile;
import com.sap.adt.abapcleaner.rulebase.RuleForCommands;
import com.sap.adt.abapcleaner.rulebase.RuleGroupID;
import com.sap.adt.abapcleaner.rulebase.RuleID;
import com.sap.adt.abapcleaner.rulebase.RuleReference;
import com.sap.adt.abapcleaner.rulebase.RuleSource;
import com.sap.adt.abapcleaner.rules.alignment.AlignParametersRule;

public class CondenseRule extends RuleForCommands {
private final static RuleReference[] references = new RuleReference[] {
new RuleReference(RuleSource.ABAP_STYLE_GUIDE, "Prefer functional to procedural language constructs", "#prefer-functional-to-procedural-language-constructs"),
new RuleReference(RuleSource.ABAP_KEYWORD_DOCU, "CONDENSE", "abapcondense.htm"),
new RuleReference(RuleSource.ABAP_KEYWORD_DOCU, "string_func - condense", "abencondense_functions.htm") };

@Override
public RuleID getID() { return RuleID.CONDENSE; }

@Override
public RuleGroupID getGroupID() { return RuleGroupID.COMMANDS; }

@Override
public String getDisplayName() { return "Replace CONDENSE with string function"; }

@Override
public String getDescription() { return "Replaces the CONDENSE statement with the string processing function condense( )."; }

@Override
public LocalDate getDateCreated() { return LocalDate.of(2024, 3, 24); }

@Override
public RuleReference[] getReferences() { return references; }

@Override
public RuleID[] getDependentRules() { return new RuleID[] { RuleID.UPPER_AND_LOWER_CASE, RuleID.ALIGN_ASSIGNMENTS, RuleID.ALIGN_PARAMETERS } ; }

// getRequiredAbapRelease() not required, as these built-in functions were introduced with ABAP release 7.02 (= 7.0, EhP2)

@Override
public boolean isEssential() { return true; }

@Override
public String getExample() {
return ""
+ LINE_SEP + " METHOD replace_condense."
+ LINE_SEP + " CONSTANTS lc_abc_with_gaps TYPE string VALUE ` a b c `."
+ LINE_SEP + ""
+ LINE_SEP + " DATA lv_text_a TYPE char30 VALUE lc_abc_with_gaps."
+ LINE_SEP + " DATA lv_text_b TYPE char30 VALUE lc_abc_with_gaps."
+ LINE_SEP + " DATA lv_text_c TYPE char30 VALUE lc_abc_with_gaps."
+ LINE_SEP + " DATA lv_string_a TYPE string VALUE lc_abc_with_gaps."
+ LINE_SEP + " DATA lv_string_b TYPE string VALUE lc_abc_with_gaps."
+ LINE_SEP + ""
+ LINE_SEP + " \" condense first text field to 'a b c', second one to 'abc'"
+ LINE_SEP + " CONDENSE lv_text_a."
+ LINE_SEP + " CONDENSE lv_text_b NO-GAPS."
+ LINE_SEP + ""
+ LINE_SEP + " \" condense first string to 'a b c', second one to 'abc'"
+ LINE_SEP + " CONDENSE lv_string_a."
+ LINE_SEP + " CONDENSE lv_string_b NO-GAPS."
+ LINE_SEP + ""
+ LINE_SEP + " \" condense text field with offset 5 and length 7 to ` a b c`"
+ LINE_SEP + " \" (specifying offset and length in write positions is possible for text fields, but not for strings)"
+ LINE_SEP + " CONDENSE lv_text_c+5(7)."
+ LINE_SEP + " ENDMETHOD.";
}

final ConfigBoolValue configSpecifyValName = new ConfigBoolValue(this, "SpecifyValName", "Explicitly specify parameter val = ... even if no other parameters are used", false);
final ConfigBoolValue configSpecifyDel = new ConfigBoolValue(this, "SpecifyDel", "Explicitly specify parameter del = ` `, except for NO-GAPS", false);
final ConfigBoolValue configSpecifyFromForNoGaps = new ConfigBoolValue(this, "SpecifyFromForNoGaps", "Explicitly specify parameter from = ` ` for NO-GAPS", true);
final ConfigBoolValue configKeepParamsOnOneLine = new ConfigBoolValue(this, "KeepParamsOnOneLine", "Keep parameters on one line (see rule '" + AlignParametersRule.DISPLAY_NAME + "', option '" + AlignParametersRule.OPTION_NAME_KEEP_OTHER_ONE_LINERS + "')", false);

private final ConfigValue[] configValues = new ConfigValue[] { configSpecifyValName, configSpecifyDel, configSpecifyFromForNoGaps, configKeepParamsOnOneLine };

@Override
public ConfigValue[] getConfigValues() { return configValues; }

public CondenseRule(Profile profile) {
super(profile);
initializeConfiguration();
}

@Override
protected boolean executeOn(Code code, Command command, int releaseRestriction) throws UnexpectedSyntaxBeforeChanges, UnexpectedSyntaxAfterChanges {
Token firstToken = command.getFirstToken();
if (firstToken == null)
return false;

// expect 'CONDENSE text [NO-GAPS].' without comments or pragmas
if (!firstToken.matchesOnSiblings(false, "CONDENSE", TokenSearch.ANY_IDENTIFIER, TokenSearch.makeOptional("NO-GAPS"), ".")) {
return false;
}

Token condenseKeyword = firstToken;
Token identifier = condenseKeyword.getNextCodeSibling();
Token next = identifier.getNextCodeSibling();
Token noGapsKeyword = null;
Token periodToken = null;
if (next.isPeriod()) {
periodToken = next;
} else {
noGapsKeyword = next;
periodToken = noGapsKeyword.getNextCodeSibling();
}
boolean noGaps = (noGapsKeyword != null);
if (!identifier.isIdentifier() || !periodToken.isPeriod()) // pro forma
return false;

int sourceLineNum = identifier.sourceLineNum;

// insert 'identifier = ' and remove CONDENSE keyword
Token assignedToIdentifier = Token.createForAbap(condenseKeyword.lineBreaks, condenseKeyword.spacesLeft, identifier.getText(), sourceLineNum);
condenseKeyword.insertLeftSibling(assignedToIdentifier);
condenseKeyword.insertLeftSibling(Token.createForAbap(0, 1, "=", sourceLineNum));
condenseKeyword.removeFromCommand();

// remove NO-GAPS
if (noGapsKeyword != null)
noGapsKeyword.removeFromCommand();

// insert 'condense( ... )' around the identifier
identifier.setWhitespace();
identifier.insertParenthesesUpTo(periodToken, "condense(", ")");
periodToken.setWhitespace(0, 0);

boolean insertParamDel = configSpecifyDel.getValue() && !noGaps;
boolean insertParamFrom = noGaps && configSpecifyFromForNoGaps.getValue();
boolean insertParamTo = noGaps;
boolean keepOnOneLine = configKeepParamsOnOneLine.getValue();

int maxParamNameLength = (insertParamFrom ? "from".length() : "val".length());

// insert 'val =' before the identifier, if specified or needed
if (configSpecifyValName.getValue() || insertParamDel || insertParamFrom || insertParamTo) {
int spacesLeft = (keepOnOneLine ? 1 : maxParamNameLength - "val".length() + 1);
identifier.insertLeftSibling(Token.createForAbap(0, 1, "val", sourceLineNum));
identifier.insertLeftSibling(Token.createForAbap(0, spacesLeft, "=", sourceLineNum));
}

// insert further parameters, if specified or needed
if (insertParamTo)
insertParam("to", "``", identifier, maxParamNameLength, keepOnOneLine);

if (insertParamFrom)
insertParam("from", "` `", identifier, maxParamNameLength, keepOnOneLine);

if (insertParamDel)
insertParam("del", "` `", identifier, maxParamNameLength, keepOnOneLine);

command.invalidateMemoryAccessType();
return true;
}

private void insertParam(String paramName, String actualValue, Token identifier, int maxParamNameLength, boolean keepOnOneLine) throws IntegrityBrokenException {
int lineBreaks = (keepOnOneLine ? 0 : 1);
int indent = (keepOnOneLine ? 1 : identifier.getParent().getEndIndexInLine() + 1);
int spacesLeftOfAssignment = (keepOnOneLine ? 1 : maxParamNameLength - paramName.length() + 1);
int sourceLineNum = identifier.sourceLineNum;

identifier.insertRightSibling(Token.createForAbap(0, 1, actualValue, sourceLineNum));
identifier.insertRightSibling(Token.createForAbap(0, spacesLeftOfAssignment, "=", sourceLineNum));
identifier.insertRightSibling(Token.createForAbap(lineBreaks, indent, paramName, sourceLineNum));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@

public abstract class RuleTestBase {
/** -1 to deactivate stress test; 7 for medium (+ 50% duration), 31 for thorough (+ 100% duration) stress test */
private static final int STRESS_TEST_TOKEN_INDEX_MAX = 7;
private static final int STRESS_TEST_TOKEN_INDEX_MAX = 31;
private static final String LINE_SEP = ABAP.LINE_SEPARATOR;

protected static Profile profile = Profile.createDefault();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.sap.adt.abapcleaner.rules.commands;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import com.sap.adt.abapcleaner.rulebase.RuleID;
import com.sap.adt.abapcleaner.rulebase.RuleTestBase;

public class CondenseTest extends RuleTestBase {
private CondenseRule rule;

CondenseTest() {
super(RuleID.CONDENSE);
rule = (CondenseRule)getRule();
}

@BeforeEach
void setUp() {
// setup default test configuration (may be modified in the individual test methods)
rule.configSpecifyValName.setValue(false);
rule.configSpecifyDel.setValue(false);
rule.configSpecifyFromForNoGaps.setValue(true);
rule.configKeepParamsOnOneLine.setValue(false);
}

@Test
void testSpecifyValName() {
rule.configSpecifyValName.setValue(true);

buildSrc(" \" comment");
buildSrc(" CONDENSE lv_text_a.");
buildSrc(" CONDENSE lv_text_b NO-GAPS.");
buildSrc(" CONDENSE lv_text_c+5(7).");

buildExp(" \" comment");
buildExp(" lv_text_a = condense( val = lv_text_a ).");
buildExp(" lv_text_b = condense( val = lv_text_b");
buildExp(" from = ` `");
buildExp(" to = `` ).");
buildExp(" lv_text_c+5(7) = condense( val = lv_text_c+5(7) ).");

putAnyMethodAroundSrcAndExp();

testRule();
}

@Test
void testSpecifyFromForNoGaps() {
buildSrc(" CONDENSE lv_text_a.");
buildSrc(" CONDENSE lv_text_b NO-GAPS.");
buildSrc(" CONDENSE lv_text_c+5(7).");

buildExp(" lv_text_a = condense( lv_text_a ).");
buildExp(" lv_text_b = condense( val = lv_text_b");
buildExp(" from = ` `");
buildExp(" to = `` ).");
buildExp(" lv_text_c+5(7) = condense( lv_text_c+5(7) ).");

putAnyMethodAroundSrcAndExp();

testRule();
}

@Test
void testDoNotSpecifyFromForNoGaps() {
rule.configSpecifyFromForNoGaps.setValue(false);

buildSrc(" CONDENSE lv_text_a.");
buildSrc(" CONDENSE lv_text_b NO-GAPS.");
buildSrc(" CONDENSE lv_text_c+5(7).");

buildExp(" lv_text_a = condense( lv_text_a ).");
buildExp(" lv_text_b = condense( val = lv_text_b");
buildExp(" to = `` ).");
buildExp(" lv_text_c+5(7) = condense( lv_text_c+5(7) ).");

putAnyMethodAroundSrcAndExp();

testRule();
}

@Test
void testSpecifyDel() {
rule.configSpecifyDel.setValue(true);

buildSrc(" CONDENSE lv_text_a.");
buildSrc(" CONDENSE lv_text_b NO-GAPS.");
buildSrc(" CONDENSE lv_text_c+5(7).");

buildExp(" lv_text_a = condense( val = lv_text_a");
buildExp(" del = ` ` ).");
buildExp(" lv_text_b = condense( val = lv_text_b");
buildExp(" from = ` `");
buildExp(" to = `` ).");
buildExp(" lv_text_c+5(7) = condense( val = lv_text_c+5(7)");
buildExp(" del = ` ` ).");

putAnyMethodAroundSrcAndExp();

testRule();
}

@Test
void testSpecifyDelButNotFrom() {
rule.configSpecifyDel.setValue(true);
rule.configSpecifyFromForNoGaps.setValue(false);

buildSrc(" CONDENSE lv_text_a.");
buildSrc(" CONDENSE lv_text_b NO-GAPS.");
buildSrc(" CONDENSE lv_text_c+5(7).");

buildExp(" lv_text_a = condense( val = lv_text_a");
buildExp(" del = ` ` ).");
buildExp(" lv_text_b = condense( val = lv_text_b");
buildExp(" to = `` ).");
buildExp(" lv_text_c+5(7) = condense( val = lv_text_c+5(7)");
buildExp(" del = ` ` ).");

putAnyMethodAroundSrcAndExp();

testRule();
}

@Test
void testCommentsAndPragmasUnchanged() {
buildSrc(" CONDENSE \" comment");
buildSrc(" lv_text_a.");
buildSrc(" CONDENSE lv_text_b NO-GAPS ##ANY_PRAGMA.");
buildSrc(" CONDENSE");
buildSrc("* comment line");
buildSrc(" lv_text_c+5(7).");

copyExpFromSrc();

putAnyMethodAroundSrcAndExp();

testRule();
}

@Test
void testKeepParamsOnOneLine() {
rule.configSpecifyDel.setValue(true);
rule.configKeepParamsOnOneLine.setValue(true);

buildSrc(" CONDENSE lv_text_a.");
buildSrc(" CONDENSE lv_text_b NO-GAPS.");
buildSrc(" CONDENSE lv_text_c+5(7).");

buildExp(" lv_text_a = condense( val = lv_text_a del = ` ` ).");
buildExp(" lv_text_b = condense( val = lv_text_b from = ` ` to = `` ).");
buildExp(" lv_text_c+5(7) = condense( val = lv_text_c+5(7) del = ` ` ).");

putAnyMethodAroundSrcAndExp();

testRule();
}
}

0 comments on commit a4382b9

Please sign in to comment.