-
Notifications
You must be signed in to change notification settings - Fork 47
/
DdlAlignLogicalExpressionsRule.java
272 lines (232 loc) · 12.8 KB
/
DdlAlignLogicalExpressionsRule.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
package com.sap.adt.abapcleaner.rules.ddl.alignment;
import java.time.LocalDate;
import com.sap.adt.abapcleaner.base.DDL;
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.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.ConfigEnumValue;
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.RuleForDdlCommands;
import com.sap.adt.abapcleaner.rulebase.RuleGroupID;
import com.sap.adt.abapcleaner.rulebase.RuleID;
import com.sap.adt.abapcleaner.rulehelpers.AlignStyle;
import com.sap.adt.abapcleaner.rulehelpers.ChangeType;
import com.sap.adt.abapcleaner.rulehelpers.LogicalExpression;
import com.sap.adt.abapcleaner.rulehelpers.TreeAlign;
import com.sap.adt.abapcleaner.rules.ddl.spaces.DdlSpacesAroundBracketsRule;
public class DdlAlignLogicalExpressionsRule extends RuleForDdlCommands {
@Override
public RuleID getID() { return RuleID.DDL_ALIGN_LOGICAL_EXPRESSIONS; }
@Override
public RuleGroupID getGroupID() { return RuleGroupID.DDL_ALIGNMENT; }
@Override
public String getDisplayName() { return "Align logical expressions in views"; }
@Override
public String getDescription() { return "Aligns logical expressions in ON / WHERE / HAVING conditions and path expressions."; }
@Override
public String getHintsAndRestrictions() { return "Space inside parentheses is configured in '" + DdlSpacesAroundBracketsRule.displayName + "'."; }
@Override
public LocalDate getDateCreated() { return LocalDate.of(2024, 9, 14); }
@Override
public String getExample() {
return ""
+ LINE_SEP + "define view entity I_AnyEntity"
+ LINE_SEP + " as select from I_AnySource as AnyAlias"
+ LINE_SEP + ""
+ LINE_SEP + " inner join I_OtherSource as OtherAlias"
+ LINE_SEP + " on OtherAlias.AnyKeyField = AnyAlias.AnyKeyField"
+ LINE_SEP + " and ( OtherAlias.AnyType = 'X' or"
+ LINE_SEP + " OtherAlias.AnyType = 'Y' )"
+ LINE_SEP + ""
+ LINE_SEP + " association [1..*] to I_ThirdSource as _ThirdSource"
+ LINE_SEP + " on AnyAlias.AnyKeyField = _ThirdSource.AnyKeyField and"
+ LINE_SEP + " OtherAlias.OtherKeyField = _ThirdSource.OtherKeyField"
+ LINE_SEP + " and OtherAlias.NumericField >= _ThirdSource.NumericField"
+ LINE_SEP + ""
+ LINE_SEP + "{"
+ LINE_SEP + " key AnyAlias.AnyKeyField,"
+ LINE_SEP + " key OtherAlias.OtherKeyField,"
+ LINE_SEP + " OtherAlias.AnyType,"
+ LINE_SEP + ""
+ LINE_SEP + " case when AnyAlias.Category = 'A' then 'category A'"
+ LINE_SEP + " when AnyAlias.Category = 'B' or"
+ LINE_SEP + " AnyAlias.Category = 'C' then 'category B or C'"
+ LINE_SEP + " end as CategoryText,"
+ LINE_SEP + ""
+ LINE_SEP + " sum(OtherAlias.NumericField) as SumField,"
+ LINE_SEP + ""
+ LINE_SEP + " max(_ThirdSource[1:SubCategory = 'X' or"
+ LINE_SEP + " SubCategory = 'Y'"
+ LINE_SEP + " or SubCategory = 'Z'].NumericField) as MaxNumericField"
+ LINE_SEP + ""
+ LINE_SEP + "}"
+ LINE_SEP + "where"
+ LINE_SEP + "OtherAlias.OtherKeyField > 'NN' and"
+ LINE_SEP + "OtherAlias.NumericField > 100"
+ LINE_SEP + "and ( AnyAlias.Category = 'A' or"
+ LINE_SEP + "AnyAlias.Category = 'B'"
+ LINE_SEP + "or AnyAlias.Category = 'C')"
+ LINE_SEP + ""
+ LINE_SEP + "group by AnyAlias.AnyKeyField,"
+ LINE_SEP + " OtherAlias.OtherKeyField,"
+ LINE_SEP + " OtherAlias.AnyType,"
+ LINE_SEP + " AnyAlias.Category"
+ LINE_SEP + ""
+ LINE_SEP + "having AnyAlias.Category = 'A' and"
+ LINE_SEP + " avg(OtherAlias.NumericField) >= 200 or"
+ LINE_SEP + " AnyAlias.Category = 'B'"
+ LINE_SEP + " and sum(OtherAlias.NumericField) >= 1000 and"
+ LINE_SEP + "sum(OtherAlias.NumericField) < 5000";
}
private static final String[] alignStyleSelectionLeftOnly = new String[] { "do not align", "left-align" }; // for IF (because this keyword is too short to be right-aligned with "AND" or "EQUIV")
private static final String[] alignStyleSelection = new String[] { "do not align", "left-align", "right-align" };
final ConfigBoolValue configAlignJoinOn = new ConfigBoolValue(this, "AlignJoinOn", "Align ON condition in JOINs", true);
final ConfigBoolValue configAlignAssociationOn = new ConfigBoolValue(this, "AlignAssociationOn", "Align ON conditions in ASSOCIATIONs", true);
final ConfigBoolValue configAlignWhen = new ConfigBoolValue(this, "AlignWhen", "Align WHEN condition in complex CASE distinctions", true);
final ConfigBoolValue configAlignPathExpressions = new ConfigBoolValue(this, "AlignPathExpressions", "Align path expressions", true);
final ConfigBoolValue configAlignWhere = new ConfigBoolValue(this, "AlignWhere", "Align WHERE clause", true);
final ConfigBoolValue configAlignHaving = new ConfigBoolValue(this, "AlignHaving", "Align HAVING clause", true);
final ConfigEnumValue<AlignStyle> configAlignOnWithBoolOps = new ConfigEnumValue<AlignStyle>(this, "AlignOnWithBoolOps", "Align AND / OR with ON", alignStyleSelectionLeftOnly, AlignStyle.values(), AlignStyle.LEFT_ALIGN);
final ConfigEnumValue<AlignStyle> configAlignFilterWithBoolOps = new ConfigEnumValue<AlignStyle>(this, "AlignFilterWithBoolOps", "Align AND / OR with FILTER", alignStyleSelection, AlignStyle.values(), AlignStyle.RIGHT_ALIGN);
final ConfigEnumValue<AlignStyle> configAlignWhenWithBoolOps = new ConfigEnumValue<AlignStyle>(this, "AlignWhenWithBoolOps", "Align AND / OR with WHEN", alignStyleSelection, AlignStyle.values(), AlignStyle.RIGHT_ALIGN);
final ConfigEnumValue<AlignStyle> configAlignWhereWithBoolOps = new ConfigEnumValue<AlignStyle>(this, "AlignWhereWithBoolOps", "Align AND / OR with WHERE", alignStyleSelection, AlignStyle.values(), AlignStyle.RIGHT_ALIGN);
final ConfigEnumValue<AlignStyle> configAlignHavingWithBoolOps = new ConfigEnumValue<AlignStyle>(this, "AlignHavingWithBoolOps", "Align AND / OR with HAVING", alignStyleSelection, AlignStyle.values(), AlignStyle.RIGHT_ALIGN);
final ConfigBoolValue configRightAlignComparisonOps = new ConfigBoolValue(this, "RightAlignComparisonOps", "Right-align comparison operators / IS", true);
final ConfigIntValue configMaxInnerSpaces = new ConfigIntValue(this, "MaxInnerSpaces", "Do not align if more than", "inner spaces would be required", 1, 20, 999);
private final ConfigValue[] configValues = new ConfigValue[] { configAlignJoinOn, configAlignAssociationOn, configAlignWhen, configAlignPathExpressions, configAlignWhere, configAlignHaving,
configAlignOnWithBoolOps, configAlignFilterWithBoolOps, configAlignWhenWithBoolOps, configAlignWhereWithBoolOps, configAlignHavingWithBoolOps,
configRightAlignComparisonOps, configMaxInnerSpaces };
@Override
public ConfigValue[] getConfigValues() { return configValues; }
public DdlAlignLogicalExpressionsRule(Profile profile) {
super(profile);
initializeConfiguration();
}
// -------------------------------------------------------------------------
private ChangeType getSpaceInsideParens() {
return ((DdlSpacesAroundBracketsRule)parentProfile.getRule(RuleID.DDL_SPACES_AROUND_BRACKETS)).getSpacesInsideArithParens();
}
@Override
protected boolean executeOn(Code code, Command command, int releaseRestriction) throws UnexpectedSyntaxBeforeChanges, UnexpectedSyntaxAfterChanges {
boolean changed = false;
Token firstToken = command.getFirstToken();
Token token = firstToken;
do {
// skip anything inside of "[DEFINE] HIERARCHY ... AS PARENT CHILD HIERARCHY( ... )", this is not yet supported:
// (conditions inside are: [DIRECTORY _directory_assoc FILTER BY cds_cond] and [START WHERE cds_cond])
if (token.isKeyword("HIERARCHY") && token.getNextCodeToken() != null && token.getNextCodeToken().getOpensLevel()) {
token = token.getNextCodeToken().getNextSibling();
continue;
}
// does this Token start a logical expression? If so, get the Token following the logical expression
Token lastInLogExpr = token.getLastTokenOfLogicalExpression();
if (lastInLogExpr != null) {
if (canAlignExpression(firstToken, token)) {
preprocessLogicalExpression(code, command, token, lastInLogExpr);
if (alignLogicalExpressions(code, command, token, lastInLogExpr)) {
changed = true;
}
}
token = lastInLogExpr.getNextCodeToken();
} else {
token = token.getNextCodeToken();
}
} while (token != null);
return changed;
}
private void preprocessLogicalExpression(Code code, Command command, Token keyword, Token lastInLogExpr) throws UnexpectedSyntaxAfterChanges {
boolean changed = false;
Token token = keyword;
Token tokenAfterKeyword = token.getNext();
if (keyword.isAnyKeyword("WHERE", "HAVING") && !tokenAfterKeyword.isComment() && tokenAfterKeyword.lineBreaks > 0) {
tokenAfterKeyword.setWhitespace();
changed = true;
}
while (token != null) {
Token prev = token.getPrev();
Token next = token.getNext();
if (token.isAnyKeyword("AND", "OR") && next.lineBreaks > 0) {
if (!token.isFirstTokenInLine())
token.copyWhitespaceFrom(next);
next.setWhitespace();
changed = true;
} else if (token.textEquals("(") && next.lineBreaks > 0 && !next.isComment()) {
next.setWhitespace();
changed = true;
} else if (token.textEquals(")") && token.isFirstTokenInLine() && !prev.isComment()) {
if (next != null && next.lineBreaks == 0)
next.copyWhitespaceFrom(token);
token.setWhitespace();
changed = true;
}
if (token == lastInLogExpr)
break;
token = token.getNext();
}
if (changed) {
code.addRuleUse(this, command);
}
}
private boolean alignLogicalExpressions(Code code, Command command, Token keyword, Token lastInLogExpr) throws UnexpectedSyntaxAfterChanges {
boolean changed = false;
try {
Token logExpStart = keyword.getNextCodeToken();
LogicalExpression logicalExpression = LogicalExpression.create(logExpStart, lastInLogExpr);
if (!logicalExpression.isSupported())
return false;
AlignStyle alignStyle = getAlignStyle(keyword);
boolean rightAlignComparisonOps = configRightAlignComparisonOps.getValue();
int maxInnerSpaces = configMaxInnerSpaces.getValue();
boolean attachParentheses = (getSpaceInsideParens() == ChangeType.NEVER);
TreeAlign treeAlign = TreeAlign.createFrom(logicalExpression);
changed = treeAlign.align(keyword, alignStyle, rightAlignComparisonOps, true, false, maxInnerSpaces, attachParentheses);
} catch (UnexpectedSyntaxException ex) {
(new UnexpectedSyntaxBeforeChanges(this, ex)).addToLog();
}
return changed;
}
private boolean canAlignExpression(Token firstToken, Token token) {
// cp. getAlignStyle() and possible keywords in Token.getLastTokenOfDdlLogicalExpression()
if (token.isAnyKeyword("ON", "FILTER")) {
if (firstToken.startsDdlJoin()) {
return configAlignJoinOn.getValue();
} else { // firstToken.startsDdlAssociation()
return configAlignAssociationOn.getValue();
}
} else if (token.isKeyword("WHEN")) {
return configAlignWhen.getValue();
} else if (token.isKeyword("WHERE")) {
return configAlignWhere.getValue();
} else if (token.isKeyword("HAVING")) {
return configAlignHaving.getValue();
} else if (token.getParent() != null && token.getParent().textEquals(DDL.BRACKET_OPEN_STRING)) {
return configAlignPathExpressions.getValue();
}
return true; // pro forma
}
private AlignStyle getAlignStyle(Token keyword) {
// cp. canAlignExpression() and possible keywords in Token.getLastTokenOfDdlLogicalExpression()
if (keyword.isKeyword("ON")) { // both for associations and joins
return AlignStyle.forValue(configAlignOnWithBoolOps.getValue());
} else if (keyword.isKeyword("FILTER")) { // "ASSOCIATION ... WITH DEFAULT FILTER"
return AlignStyle.forValue(configAlignFilterWithBoolOps.getValue());
} else if (keyword.isKeyword("WHEN")) {
return AlignStyle.forValue(configAlignWhenWithBoolOps.getValue());
} else if (keyword.isKeyword("WHERE")) {
return AlignStyle.forValue(configAlignWhereWithBoolOps.getValue());
} else if (keyword.isKeyword("HAVING")) {
return AlignStyle.forValue(configAlignHavingWithBoolOps.getValue());
} else if (keyword.getParent() != null && keyword.getParent().textEquals(DDL.BRACKET_OPEN_STRING)) {
// path expressions
return AlignStyle.DO_NOT_ALIGN;
} else { // pro forma
return AlignStyle.DO_NOT_ALIGN;
}
}
}