Skip to content

Commit

Permalink
add new rule "Align FORM declarations" (#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmgrassau committed Nov 1, 2023
1 parent 4243730 commit 3962fb4
Show file tree
Hide file tree
Showing 6 changed files with 516 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ final void clearUsedRules() {
usedRules.clear();
}

/** returns true if the setting was changed */
/** returns true if the setting was changed */
public final boolean setBlockedRule(RuleID ruleId, boolean blocked) {
if (blockedRules.get(ruleId.getValue()) == blocked) {
return false;
Expand Down
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 = 62;
public static final int RULE_COUNT = 63;
public static final int RULE_GROUP_COUNT = 7;

protected static final String LINE_SEP = ABAP.LINE_SEPARATOR;
Expand Down Expand Up @@ -131,7 +131,8 @@ static Rule[] getAllRules(Profile profile) {
new AlignClearFreeAndSortRule(profile),
new AlignParametersRule(profile),
new AlignLogicalExpressionsRule(profile),
new AlignCondExpressionsRule(profile)
new AlignCondExpressionsRule(profile),
new AlignFormDeclarationRule(profile)
};

StringBuilder errors = new StringBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ public enum RuleID {
ALIGN_CLEAR_FREE_AND_SORT,
ALIGN_PARAMETERS,
ALIGN_LOGICAL_EXPRESSIONS,
ALIGN_COND_EXPRESSIONS;
ALIGN_COND_EXPRESSIONS,
ALIGN_FORM_DECLARATION;

public static final int SIZE = java.lang.Integer.SIZE;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package com.sap.adt.abapcleaner.rules.alignment;

import java.time.LocalDate;

import com.sap.adt.abapcleaner.base.ABAP;
import com.sap.adt.abapcleaner.parser.Code;
import com.sap.adt.abapcleaner.parser.Command;
import com.sap.adt.abapcleaner.parser.Term;
import com.sap.adt.abapcleaner.parser.Token;
import com.sap.adt.abapcleaner.programbase.UnexpectedSyntaxAfterChanges;
import com.sap.adt.abapcleaner.programbase.UnexpectedSyntaxBeforeChanges;
import com.sap.adt.abapcleaner.programbase.UnexpectedSyntaxException;
import com.sap.adt.abapcleaner.rulebase.ConfigBoolValue;
import com.sap.adt.abapcleaner.rulebase.ConfigIntValue;
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.rulehelpers.AlignCellTerm;
import com.sap.adt.abapcleaner.rulehelpers.AlignCellToken;
import com.sap.adt.abapcleaner.rulehelpers.AlignLine;
import com.sap.adt.abapcleaner.rulehelpers.AlignTable;

public class AlignFormDeclarationRule extends RuleForCommands {
private enum Columns {
FORM,
FORM_NAME,
PARAMETER_GROUP,
PARAMETER_NAME,
TYPE_OR_LIKE;

public int getValue() { return this.ordinal(); }
}
private static final int MAX_COLUMN_COUNT = 5;

private static final String[] parameterGroups = new String[] { "TABLES", "USING", "CHANGING", "RAISING" };

private final static RuleReference[] references = new RuleReference[] { new RuleReference(RuleSource.ABAP_CLEANER) };

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

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

@Override
public String getDisplayName() { return "Align FORM declarations"; }

@Override
public String getDescription() { return "Aligns (obsolete) FORM declarations."; }

@Override
public LocalDate getDateCreated() { return LocalDate.of(2023, 10, 31); }

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

@Override
public String getExample() {
return LINE_SEP + "FORM any_form USING iv_any_value TYPE string."
+ LINE_SEP
+ LINE_SEP + " \" any FORM implementation"
+ LINE_SEP + "ENDFORM."
+ LINE_SEP
+ LINE_SEP
+ LINE_SEP + "FORM other_form USING iv_any_value TYPE i iv_other_value TYPE string CHANGING cv_third_value TYPE i."
+ LINE_SEP + " \" other FORM implementation"
+ LINE_SEP + "ENDFORM."
+ LINE_SEP
+ LINE_SEP
+ LINE_SEP + "FORM third_form_with_a_long_name TABLES it_any_table STRUCTURE ty_s_any_struc"
+ LINE_SEP + " it_other_table TYPE STANDARD TABLE it_third_table it_fourth_table TYPE ty_tt_any"
+ LINE_SEP + " CHANGING ct_table TYPE ty_tt_table cs_struc TYPE LINE OF ty_tt_any cs_other_struc LIKE cs_any"
+ LINE_SEP + " cs_third_struc LIKE LINE OF ct_table."
+ LINE_SEP + " \" third FORM implementation"
+ LINE_SEP + "ENDFORM."
+ LINE_SEP
+ LINE_SEP
+ LINE_SEP + "FORM fourth_form"
+ LINE_SEP + " USING"
+ LINE_SEP + " VALUE(iv_any) TYPE string"
+ LINE_SEP + " iv_other TYPE REF TO object"
+ LINE_SEP + " RAISING"
+ LINE_SEP + " cx_any_exception RESUMABLE(cx_other_exception) cx_third_exception."
+ LINE_SEP + " \" fourth FORM implementation"
+ LINE_SEP + "ENDFORM.";
}

final ConfigIntValue configParamCountBehindFormName = new ConfigIntValue(this, "ParamCountBehindFormName", "Continue after FORM name for up to", "parameters", 0, 3, 100);
final ConfigBoolValue configContinueAfterParamGroupKeyword = new ConfigBoolValue(this, "ContinueAfterParamGroupKeyword", "Continue after TABLES / USING / CHANGING / RAISING", true);
final ConfigBoolValue configAlignTypes = new ConfigBoolValue(this, "AlignTypes", "Align TYPEs", true);
final ConfigBoolValue configAddEmptyLine = new ConfigBoolValue(this, "AddEmptyLine", "Add empty line after multi-line FORM declaration", true);
final ConfigBoolValue configRemoveEmptyLine = new ConfigBoolValue(this, "RemoveEmptyLine", "Remove empty line after one-line FORM declaration", true);

private final ConfigValue[] configValues = new ConfigValue[] { configParamCountBehindFormName, configContinueAfterParamGroupKeyword, configAlignTypes, configAddEmptyLine, configRemoveEmptyLine };

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

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

@Override
protected boolean executeOn(Code code, Command command, int releaseRestriction) throws UnexpectedSyntaxBeforeChanges, UnexpectedSyntaxAfterChanges {
if (!command.firstCodeTokenIsKeyword("FORM"))
return false;

// build the align table
AlignTable table = new AlignTable(MAX_COLUMN_COUNT);
AlignLine line = table.addLine();

// add FORM keyword
Token formKeyword = command.getFirstCodeToken();
line.setCell(Columns.FORM.getValue(), new AlignCellToken(formKeyword));

// add FORM name
Token formName = formKeyword.getNextCodeSibling();
if (!formName.isIdentifier())
return false;
line.setCell(Columns.FORM_NAME.getValue(), new AlignCellToken(formName));

boolean isInRaisingGroup = false;
Token token = formName.getNextCodeSibling();
while(token != null && !token.isPeriod()) {
// add parameter group keyword
if (token.isAnyKeyword(parameterGroups)) {
isInRaisingGroup = token.isKeyword("RAISING");
line.setCell(Columns.PARAMETER_GROUP.getValue(), new AlignCellToken(token));
token = token.getNextCodeSibling();
}

// add identifier
if (token.isIdentifier()) {
line.setCell(Columns.PARAMETER_NAME.getValue(), new AlignCellToken(token));
token = token.getNextCodeSibling();
} else if (token.textEqualsAny("VALUE(", "RESUMABLE(")) {
Term parameterTerm;
try {
parameterTerm = Term.createForTokenRange(token, token.getNextSibling());
} catch (UnexpectedSyntaxException e) {
throw new UnexpectedSyntaxBeforeChanges(this, e);
}
line.setCell(Columns.PARAMETER_NAME.getValue(), new AlignCellTerm(parameterTerm));
token = parameterTerm.lastToken.getNextCodeSibling();
} else {
throw new UnexpectedSyntaxBeforeChanges(this, token, "Unexpected parameter for a FORM definition");
}
if (isInRaisingGroup) {
// if TYPEs are aligned, do not let exception names influence the width of the PARAMETER_NAME column
line.getCell(Columns.PARAMETER_NAME.getValue()).setOverrideTextWidth(1);
}
if (token == null || token.isPeriod())
break;

// add TYPE section (if any)
if (!isInRaisingGroup && token.isAnyKeyword("TYPE", "LIKE", "STRUCTURE")) {
Token typeStart = token;
if (token.isKeyword("STRUCTURE")) {
token = token.getNextCodeSibling();
} else { // "TYPE", "LIKE"
// read until the last token, which is
// - the keyword 'TABLE' for the (always generic) 'TYPE {ANY|HASHED|INDEX|SORTED|STANDARD} TABLE'
// - the identifier of a complete type (possibly after 'LINE OF', 'REF TO'), e.g. i, abap_bool, char30,
// ty_s_any, struc-component, class=>attribute, LINE OF ty_tt_table, REF TO cl_any_class
// - the identifier of a generic type: any, c, clike, csequence, data, decfloat, n, numeric, REF TO object,
// p, simple, table, x, xsequence
do {
token = token.getNextCodeSibling();
} while (token != null && !token.isIdentifier() && !token.isKeyword("TABLE"));
}
Term typeTerm;
try {
typeTerm = Term.createForTokenRange(typeStart, token);
} catch (UnexpectedSyntaxException e) {
throw new UnexpectedSyntaxBeforeChanges(this, e);
}
line.setCell(Columns.TYPE_OR_LIKE.getValue(), new AlignCellTerm(typeTerm));
token = token.getNextCodeSibling();
}

line = table.addLine();
}

if (table.getLineCount() > 1 && line.getCell(Columns.PARAMETER_NAME.getValue()) == null)
table.removeLastLine();

int basicIndent = command.getFirstToken().spacesLeft;
boolean breakAfterFormName = (table.getLineCount() > configParamCountBehindFormName.getValue());
if (breakAfterFormName) {
table.getColumn(Columns.FORM_NAME.getValue()).setForceLineBreakAfter(false);
table.getColumn(Columns.PARAMETER_GROUP.getValue()).setForceIndent(basicIndent + ABAP.INDENT_STEP);
}
if (!configContinueAfterParamGroupKeyword.getValue()) {
table.getColumn(Columns.PARAMETER_GROUP.getValue()).setForceLineBreakAfter(false);
int paramGroupIndent = basicIndent + (breakAfterFormName ? ABAP.INDENT_STEP : formKeyword.getTextLength() + 1 + formName.getTextLength() + 1);
table.getColumn(Columns.PARAMETER_NAME.getValue()).setForceIndent(paramGroupIndent + ABAP.INDENT_STEP);
}
if (!configAlignTypes.getValue()) {
try {
table.getColumn(Columns.TYPE_OR_LIKE.getValue()).joinIntoPreviousColumns(true);
} catch (UnexpectedSyntaxException e) {
throw new UnexpectedSyntaxAfterChanges(this, e);
}
}

int firstLineBreaks = command.getFirstToken().lineBreaks;
Command[] changedCommands = table.align(basicIndent, firstLineBreaks, false, true);
for (Command changedCommand : changedCommands) {
code.addRuleUse(this, changedCommand);
}

// if configured and required, add or remove an empty line above the first Command in the FORM implementation
if (command.hasChildren()) {
boolean isMultiLine = command.containsInnerLineBreaks(false);
Command nextCommand = command.getNext();
Token nextToken = nextCommand.getFirstToken();

if (isCommandBlocked(nextCommand)) {
// do nothing
} else if (isMultiLine && nextToken.lineBreaks < 2 && configAddEmptyLine.getValue()) {
nextToken.lineBreaks = 2;
code.addRuleUse(this, nextCommand);
} else if (!isMultiLine && nextToken.lineBreaks >= 2 && configRemoveEmptyLine.getValue()) {
nextToken.lineBreaks = 1;
code.addRuleUse(this, nextCommand);
}
}
return false; // code.addRuleUse was already called above
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;

import com.sap.adt.abapcleaner.base.ABAP;
import com.sap.adt.abapcleaner.base.StringUtil;
Expand Down Expand Up @@ -59,7 +60,8 @@ public abstract class RuleTestBase {

private boolean checkSyntaxAfterParse = true;
private boolean checkRuleWasUsed = true;

private HashSet<Integer> commandIndicesToBlock = new HashSet<>();

protected RuleTestBase(RuleID ruleID) {
this.ruleID = ruleID;
this.abapReleaseOfCode = ABAP.NEWEST_RELEASE;
Expand All @@ -82,7 +84,7 @@ void basicSetUp() {
// tests for each rule (executed once for each concrete child class of RuleTestBase)

@Test
void testExampleCode() {
void testExampleCode() {
String sourceCode = getRule().getExample();
assertFalse(StringUtil.isNullOrEmpty(sourceCode));

Expand Down Expand Up @@ -165,6 +167,10 @@ protected void deactivateRuleUseCheck() {
checkRuleWasUsed = false;
}

protected void blockCommand(int commandIndex) {
commandIndicesToBlock.add(commandIndex);
}

@Test
void testRuleInfo() {
Rule rule = getRule();
Expand Down Expand Up @@ -232,6 +238,9 @@ protected void testRule() {
command = command.getNext();
}

// apply prepared blocking to specific commands
blockCommands(code);

// call the Rule under test and get the actual result code
try {
getRule().executeIfAllowedOn(code, releaseRestrictionFromUI);
Expand Down Expand Up @@ -405,6 +414,20 @@ protected void testRule() {
}
}

private void blockCommands(Code code) {
if (commandIndicesToBlock.isEmpty())
return;
Command command = code.firstCommand;
int index = 0;
while (command != null) {
if (commandIndicesToBlock.contains(index)) {
command.getChangeControl().setBlockedRule(ruleID, true);
}
++index;
command = command.getNext();
}
}

private boolean runStressTest(StressTestType stressTestType, int insertAfterTokenIndex) {
// parse the source code
Code code = null;
Expand All @@ -413,6 +436,9 @@ private boolean runStressTest(StressTestType stressTestType, int insertAfterToke
} catch (ParseException e) {
fail(e.getMessage());
}

// apply prepared blocking to specific commands
blockCommands(code);

String stressTestInfo = " [stress test: inserted " + stressTestType.description + " after token #" + String.valueOf(insertAfterTokenIndex) + "]";
try {
Expand Down
Loading

0 comments on commit 3962fb4

Please sign in to comment.