Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 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 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 15478x 15478x 15478x 15478x 15478x 165549x 165549x 165549x 165549x 165549x 165549x 165549x 165549x 165549x 165549x 15478x 15478x 14908x 14908x 15478x 15478x 264x 264x 15478x 15478x 315x 315x 315x 12x 12x 303x 303x 303x 303x 303x 315x 78x 78x 14x 14x 78x 303x 315x 78x 78x 64x 64x 78x 14x 78x 14x 14x 78x 1x 1x 13x 13x 13x 13x 13x 13x 13x 13x 13x 303x 303x 303x 15478x 15478x 117x 117x 20x 20x 117x 63x 63x 34x 34x 15478x 15478x 13x 13x 32x 19x 19x 19x 19x 32x 13x 13x 32x 13x 13x 15478x 15478x 15478x 49x 49x 49x 49x 15478x 15478x | import {Issue} from "../issue";
import {ABAPRule} from "./_abap_rule";
import * as Expressions from "../abap/2_statements/expressions";
import {BasicRuleConfig} from "./_basic_rule_config";
import {IRuleMetadata, RuleTag} from "./_irule";
import {ABAPFile} from "../abap/abap_file";
import {ExpressionNode} from "../abap/nodes";
import {EditHelper} from "../edit_helper";
export class PreferStringTemplateConf extends BasicRuleConfig {
}
export class PreferStringTemplate extends ABAPRule {
private conf = new PreferStringTemplateConf();
public getMetadata(): IRuleMetadata {
return {
key: "prefer_string_template",
title: "Prefer string template over &&",
shortDescription: `Use string templates instead of && to assemble text`,
extendedInformation: `https://github.com/SAP/styleguides/blob/main/clean-abap/CleanABAP.md#use--to-assemble-text`,
tags: [RuleTag.Styleguide, RuleTag.SingleFile, RuleTag.Quickfix],
badExample: `DATA(msg) = \`Hello \` && name && \`!\`.`,
goodExample: `DATA(msg) = |Hello { name }!|.`,
};
}
public getConfig() {
return this.conf;
}
public setConfig(conf: PreferStringTemplateConf) {
this.conf = conf;
}
public runParsed(file: ABAPFile): Issue[] {
const issues: Issue[] = [];
const structure = file.getStructure();
if (structure === undefined) {
return [];
}
const allSources = structure.findAllExpressions(Expressions.Source);
// collect all Source nodes that appear as the RHS (children[2]) of a && expression
const rhsChildren = new Set<ExpressionNode>();
for (const source of allSources) {
const children = source.getChildren();
if (children.length === 3 && children[1].getFirstToken().getStr() === "&&") {
rhsChildren.add(children[2] as ExpressionNode);
}
}
for (const source of allSources) {
const children = source.getChildren();
if (children.length !== 3) {
continue;
}
if (children[1].getFirstToken().getStr() !== "&&") {
continue;
}
// only process top-level && roots (not nested RHS children)
if (rhsChildren.has(source)) {
continue;
}
const operands = this.flattenChain(source);
if (!operands.some(op => this.isLiteral(op))) {
continue;
}
const template = this.buildTemplate(operands);
const start = source.getFirstToken().getStart();
const end = source.getLastToken().getEnd();
const fix = EditHelper.replaceRange(file, start, end, template);
const token = children[1].getFirstToken();
issues.push(Issue.atToken(file, token, "Use string template instead of &&", this.getMetadata().key, this.conf.severity, fix));
}
return issues;
}
private flattenChain(node: ExpressionNode): ExpressionNode[] {
const children = node.getChildren();
if (children.length === 3 && children[1].getFirstToken().getStr() === "&&") {
return [...this.flattenChain(children[0] as ExpressionNode), ...this.flattenChain(children[2] as ExpressionNode)];
}
if (children.length === 1 && children[0] instanceof ExpressionNode) {
return this.flattenChain(children[0]);
}
return [node];
}
private buildTemplate(operands: ExpressionNode[]): string {
let inner = "";
for (const op of operands) {
if (this.isLiteral(op)) {
const raw = op.getFirstToken().getStr();
const delimiter = raw[0];
const value = raw.slice(1, -1).split(delimiter + delimiter).join(delimiter);
inner += value.replace(/[\\|{}]/g, "\\$&");
} else {
inner += `{ ${op.concatTokens()} }`;
}
}
return `|${inner}|`;
}
// plain character or string literal, ie. not text symbols or concatenated constants
private isLiteral(op: ExpressionNode): boolean {
if (op.getFirstToken() !== op.getLastToken()) {
return false;
}
const firstChar = op.getFirstToken().getStr()[0];
return firstChar === "'" || firstChar === "`";
}
}
|