@abaplint/core 2.120.77 → 2.120.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,7 @@ export declare abstract class ABAPObject extends AbstractObject {
23
23
  } | undefined;
24
24
  private textsTranslations;
25
25
  private rawXMLCache;
26
+ private parsing;
26
27
  syntaxResult: ISyntaxResult | undefined;
27
28
  abstract getSequencedFiles(): readonly ABAPFile[];
28
29
  abstract getDescription(): string | undefined;
@@ -7808,6 +7809,7 @@ declare type SyntaxInput = {
7808
7809
 
7809
7810
  export declare class SyntaxLogic {
7810
7811
  private currentFile;
7812
+ private readonly includeStack;
7811
7813
  private issues;
7812
7814
  private deferred;
7813
7815
  private readonly object;
@@ -92,7 +92,7 @@ class ExpandMacros {
92
92
  listMacroNames() {
93
93
  return this.macros.listMacroNames();
94
94
  }
95
- find(statements, file, clear = true) {
95
+ find(statements, file, clear = true, includePath = new Set()) {
96
96
  var _a, _b, _c;
97
97
  let nameToken = undefined;
98
98
  let start = undefined;
@@ -113,14 +113,16 @@ class ExpandMacros {
113
113
  else if (type instanceof Statements.Include) {
114
114
  const includeName = (_b = statement.findDirectExpression(Expressions.IncludeName)) === null || _b === void 0 ? void 0 : _b.concatTokens();
115
115
  // todo, this does not take function module includes into account
116
- // todo, workaround for cyclic includes?
117
116
  const prog = (_c = this.reg) === null || _c === void 0 ? void 0 : _c.getObject("PROG", includeName);
118
117
  if (prog) {
119
118
  prog.parse(this.release, this.globalMacros, this.reg, this.languageVersion);
120
119
  const includeMainFile = prog.getMainABAPFile();
121
- if (includeMainFile) {
120
+ // an include that is already on the current include path is not walked again
121
+ if (includeMainFile && includePath.has(includeMainFile.getFilename()) === false) {
122
+ includePath.add(includeMainFile.getFilename());
122
123
  // slow, this copies everything,
123
- this.find([...includeMainFile.getStatements()], includeMainFile, false);
124
+ this.find([...includeMainFile.getStatements()], includeMainFile, false, includePath);
125
+ includePath.delete(includeMainFile.getFilename());
124
126
  }
125
127
  }
126
128
  }
@@ -16,11 +16,11 @@ class Convert {
16
16
  const dat = (0, combi_1.seq)("DATE", expressions_1.Source);
17
17
  const tim = (0, combi_1.seq)("TIME", expressions_1.Source);
18
18
  const stamp = (0, combi_1.seq)("INTO TIME STAMP", expressions_1.Target);
19
- const intoutc = (0, combi_1.ver)(version_1.Release.v754, (0, combi_1.seq)("INTO UTCLONG", expressions_1.Target));
19
+ const intoutc = (0, combi_1.ver)(version_1.Release.v754, (0, combi_1.seq)("INTO UTCLONG", expressions_1.Target), { also: combi_1.AlsoIn.OpenABAP });
20
20
  const invert = (0, combi_1.seq)("INTO INVERTED-DATE", expressions_1.Target);
21
21
  const date = (0, combi_1.seq)((0, combi_1.per)(dat, tim), (0, combi_1.per)(daylightSource, stamp, zone, invert, intoutc));
22
22
  const inv = (0, combi_1.seq)("INVERTED-DATE", expressions_1.Source, "INTO DATE", expressions_1.Target);
23
- const utclong = (0, combi_1.ver)(version_1.Release.v754, (0, combi_1.seq)("UTCLONG", expressions_1.Source, (0, combi_1.per)(zone, into, daylightSource)), { also: combi_1.AlsoIn.OpenABAP });
23
+ const utclong = (0, combi_1.ver)(version_1.Release.v754, (0, combi_1.seq)("UTCLONG", expressions_1.Source, (0, combi_1.per)(zone, into, daylightTarget)), { also: combi_1.AlsoIn.OpenABAP });
24
24
  return (0, combi_1.seq)("CONVERT", (0, combi_1.alt)(time, date, inv, utclong));
25
25
  }
26
26
  }
@@ -1194,9 +1194,9 @@ BuiltIn.methods = {
1194
1194
  },
1195
1195
  optional: {
1196
1196
  "days": basic_1.IntegerType.get(),
1197
- "hour": basic_1.IntegerType.get(),
1198
- "minutes": basic_1.IntegerType.get(),
1199
- "seconds": new basic_1.FloatType(),
1197
+ "hours": basic_1.IntegerType.get(),
1198
+ "minutes": new basic_1.Integer8Type(),
1199
+ "seconds": new basic_1.DecFloat34Type(),
1200
1200
  },
1201
1201
  return: new basic_1.UTCLongType(),
1202
1202
  release: version_1.Release.v754,
@@ -1212,7 +1212,7 @@ BuiltIn.methods = {
1212
1212
  "high": new basic_1.UTCLongType(),
1213
1213
  "low": new basic_1.UTCLongType(),
1214
1214
  },
1215
- return: new basic_1.FloatType(),
1215
+ return: new basic_1.DecFloat34Type(),
1216
1216
  release: version_1.Release.v754,
1217
1217
  },
1218
1218
  "XSDBOOL": {
@@ -52,7 +52,12 @@ class Procedural {
52
52
  this.scope = scope;
53
53
  this.reg = reg;
54
54
  }
55
- addAllFormDefinitions(file, obj) {
55
+ addAllFormDefinitions(file, obj, visited = new Set()) {
56
+ // an include reached again through a cycle of INCLUDEs is not walked twice
57
+ if (visited.has(file.getFilename())) {
58
+ return;
59
+ }
60
+ visited.add(file.getFilename());
56
61
  const structure = file.getStructure();
57
62
  if (structure) {
58
63
  const input = {
@@ -73,12 +78,12 @@ class Procedural {
73
78
  for (const node of includes) {
74
79
  const found = this.findInclude(node, obj);
75
80
  if (found) {
76
- this.addAllFormDefinitions(found, obj);
81
+ this.addAllFormDefinitions(found, obj, visited);
77
82
  }
78
83
  }
79
84
  }
80
85
  findInclude(node, obj) {
81
- // assumption: no cyclic includes, includes not found are reported by rule "check_include"
86
+ // cyclic includes are cut by the callers, includes not found are reported by rule "check_include"
82
87
  // todo: how to make sure code is not duplicated here and in rule "check_include" / include graph?
83
88
  const expr = node.findFirstExpression(Expressions.IncludeName);
84
89
  if (expr === undefined) {
@@ -42,7 +42,11 @@ const _syntax_input_1 = require("../_syntax_input");
42
42
  // On a character or byte field of fixed length the substring must lie inside the field, else it is
43
43
  // a syntax error. Other types, a dynamic offset or length and "*" (undefined) are left alone, as is
44
44
  // length 0. The offset is applied first, so the length is compared to what remains after the offset.
45
+ // A substring of a date, time or numeric text field has type n; its bounds are not checked.
45
46
  function substringType(context, offset, length, node, input) {
47
+ if (context instanceof basic_1.DateType || context instanceof basic_1.TimeType || context instanceof basic_1.NumericType) {
48
+ return numericSubstringType(context, offset, length);
49
+ }
46
50
  if (!(context instanceof basic_1.CharacterType) && !(context instanceof basic_1.HexType)) {
47
51
  return context;
48
52
  }
@@ -65,6 +69,24 @@ function substringType(context, offset, length, node, input) {
65
69
  }
66
70
  return context;
67
71
  }
72
+ function numericSubstringType(context, offset, length) {
73
+ const hasOffset = offset !== undefined && offset > 0;
74
+ const hasLength = length !== undefined && length > 0;
75
+ if (hasOffset === false && hasLength === false) {
76
+ return context;
77
+ }
78
+ let remaining = context instanceof basic_1.DateType ? 8 : context instanceof basic_1.TimeType ? 6 : context.getLength();
79
+ if (hasOffset) {
80
+ remaining = remaining - offset;
81
+ }
82
+ if (hasLength) {
83
+ remaining = Math.min(remaining, length);
84
+ }
85
+ if (remaining <= 0) {
86
+ return context;
87
+ }
88
+ return new basic_1.NumericType(remaining);
89
+ }
68
90
  // A field symbol typed "c" or "x" is generic, but its type is represented with length 1, so the
69
91
  // offset or length at "index" is not checked when it directly follows the field symbol
70
92
  function isFieldSymbolItself(children, index) {
@@ -82,6 +82,16 @@ class Convert {
82
82
  target_1.Target.runSyntax(stampTarget, input);
83
83
  }
84
84
  }
85
+ const utclongTarget = node.findExpressionAfterToken("UTCLONG");
86
+ if ((utclongTarget === null || utclongTarget === void 0 ? void 0 : utclongTarget.get()) instanceof Expressions.Target) {
87
+ const inline = utclongTarget === null || utclongTarget === void 0 ? void 0 : utclongTarget.findDirectExpression(Expressions.InlineData);
88
+ if (inline) {
89
+ inline_data_1.InlineData.runSyntax(inline, input, new basic_1.UTCLongType());
90
+ }
91
+ else {
92
+ target_1.Target.runSyntax(utclongTarget, input);
93
+ }
94
+ }
85
95
  }
86
96
  }
87
97
  exports.Convert = Convert;
@@ -337,6 +337,8 @@ if (Object.keys(map).length === 0) {
337
337
  // -----------------------------------
338
338
  class SyntaxLogic {
339
339
  constructor(reg, object) {
340
+ // files the traversal walked into through INCLUDEs, outermost first
341
+ this.includeStack = [];
340
342
  this.reg = reg;
341
343
  this.issues = [];
342
344
  this.deferred = [];
@@ -444,11 +446,20 @@ class SyntaxLogic {
444
446
  // walk into INCLUDEs
445
447
  if (isStatement && child.get() instanceof Statements.Include) {
446
448
  const file = this.helpers.proc.findInclude(child, this.object);
447
- if (file !== undefined && file.getStructure() !== undefined) {
449
+ // an INCLUDE that leads back to a file already being walked is not followed again
450
+ const cyclic = file !== undefined && (file.getFilename() === this.currentFile.getFilename()
451
+ || this.includeStack.includes(file.getFilename()));
452
+ if (file !== undefined && file.getStructure() !== undefined && cyclic === false) {
448
453
  const old = this.currentFile;
454
+ this.includeStack.push(old.getFilename());
449
455
  this.currentFile = file;
450
- this.traverse(file.getStructure());
451
- this.currentFile = old;
456
+ try {
457
+ this.traverse(file.getStructure());
458
+ }
459
+ finally {
460
+ this.currentFile = old;
461
+ this.includeStack.pop();
462
+ }
452
463
  }
453
464
  }
454
465
  if (isStructure || isStatement) {
@@ -1,9 +1,44 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.CrossIncludeMacros = void 0;
4
37
  const _abap_object_1 = require("../objects/_abap_object");
5
38
  const program_1 = require("../objects/program");
6
39
  const _statement_1 = require("./2_statements/statements/_statement");
40
+ const statements_1 = require("./2_statements/statements");
41
+ const Expressions = __importStar(require("./2_statements/expressions"));
7
42
  const expand_macros_1 = require("./2_statements/expand_macros");
8
43
  const include_graph_1 = require("../utils/include_graph");
9
44
  // Macros can be defined in one include and used in a sibling include, connected
@@ -16,6 +51,7 @@ class CrossIncludeMacros {
16
51
  this.reg = reg;
17
52
  }
18
53
  run() {
54
+ this.reparseCyclicPrograms();
19
55
  const candidates = this.findCandidates();
20
56
  if (candidates.length === 0) {
21
57
  return;
@@ -43,6 +79,45 @@ class CrossIncludeMacros {
43
79
  }
44
80
  }
45
81
  //////////////////////////////
82
+ // A program or include in an INCLUDE cycle can be parsed while the include defining its macros is still
83
+ // being parsed, depending on the order the files were added. Everything is parsed now, so parse it again.
84
+ reparseCyclicPrograms() {
85
+ const config = this.reg.getConfig();
86
+ for (const obj of this.reg.getObjects()) {
87
+ if (obj instanceof program_1.Program && this.hasUnknown(obj) && this.isInIncludeCycle(obj)) {
88
+ obj.setDirty();
89
+ obj.parse(config.getRelease(), config.getSyntaxSetttings().globalMacros, this.reg, config.getLanguageVersion());
90
+ }
91
+ }
92
+ }
93
+ hasUnknown(prog) {
94
+ return prog.getABAPFiles().some(f => f.getStatements().some(s => s.get() instanceof _statement_1.Unknown));
95
+ }
96
+ // the include graph does not link INCLUDEs of main programs, so follow the INCLUDEs from the program
97
+ isInIncludeCycle(prog) {
98
+ var _a;
99
+ const seen = new Set();
100
+ const stack = [prog];
101
+ while (stack.length > 0) {
102
+ for (const file of stack.pop().getABAPFiles()) {
103
+ for (const statement of file.getStatements()) {
104
+ if (!(statement.get() instanceof statements_1.Include)) {
105
+ continue;
106
+ }
107
+ const name = (_a = statement.findDirectExpression(Expressions.IncludeName)) === null || _a === void 0 ? void 0 : _a.concatTokens();
108
+ const included = this.reg.getObject("PROG", name);
109
+ if (included === prog) {
110
+ return true;
111
+ }
112
+ else if (included instanceof program_1.Program && seen.has(included) === false) {
113
+ seen.add(included);
114
+ stack.push(included);
115
+ }
116
+ }
117
+ }
118
+ }
119
+ return false;
120
+ }
46
121
  findCandidates() {
47
122
  const ret = [];
48
123
  for (const obj of this.reg.getObjects()) {
@@ -11,6 +11,7 @@ class ABAPObject extends _abstract_object_1.AbstractObject {
11
11
  }
12
12
  constructor(name) {
13
13
  super(name);
14
+ this.parsing = false;
14
15
  this.parsed = [];
15
16
  this.texts = undefined;
16
17
  this.rawXMLCache = undefined;
@@ -19,11 +20,19 @@ class ABAPObject extends _abstract_object_1.AbstractObject {
19
20
  return !!x && x instanceof ABAPObject;
20
21
  }
21
22
  parse(release, globalMacros, reg, languageVersion = version_1.LanguageVersion.Normal) {
22
- if (this.isDirty() === false) {
23
+ // an INCLUDE that leads back to this object while it is being parsed is not followed again
24
+ if (this.isDirty() === false || this.parsing === true) {
23
25
  return { updated: false, runtime: 0 };
24
26
  }
25
27
  const abapFiles = this.getFiles().filter(f => f.getFilename().endsWith(".abap"));
26
- const result = new abap_parser_1.ABAPParser({ release, globalMacros, reg, languageVersion }).parse(abapFiles);
28
+ let result;
29
+ this.parsing = true;
30
+ try {
31
+ result = new abap_parser_1.ABAPParser({ release, globalMacros, reg, languageVersion }).parse(abapFiles);
32
+ }
33
+ finally {
34
+ this.parsing = false;
35
+ }
27
36
  this.parsed = result.output;
28
37
  this.old = result.issues;
29
38
  this.dirty = false;
@@ -75,7 +75,7 @@ class Registry {
75
75
  }
76
76
  static abaplintVersion() {
77
77
  // magic, see build script "version.js"
78
- return "2.120.77";
78
+ return "2.120.79";
79
79
  }
80
80
  getDDICReferences() {
81
81
  return this.ddicReferences;
@@ -105,6 +105,7 @@ __exportStar(require("./inline_data_old_versions"), exports);
105
105
  __exportStar(require("./inline_packed_computation"), exports);
106
106
  __exportStar(require("./intf_referencing_clas"), exports);
107
107
  __exportStar(require("./invalid_table_index"), exports);
108
+ __exportStar(require("./is_assigned_in_loop"), exports);
108
109
  __exportStar(require("./keep_single_parameter_on_one_line"), exports);
109
110
  __exportStar(require("./keyword_case"), exports);
110
111
  __exportStar(require("./line_break_multiple_parameters"), exports);
@@ -170,6 +171,7 @@ __exportStar(require("./prefix_is_current_class"), exports);
170
171
  __exportStar(require("./range_row_values"), exports);
171
172
  __exportStar(require("./reduce_procedural_code"), exports);
172
173
  __exportStar(require("./reduce_string_templates"), exports);
174
+ __exportStar(require("./redundant_cast"), exports);
173
175
  __exportStar(require("./redundant_conversion"), exports);
174
176
  __exportStar(require("./release_idoc"), exports);
175
177
  __exportStar(require("./remove_descriptions"), exports);
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.IsAssignedInLoop = exports.IsAssignedInLoopConf = void 0;
37
+ const Statements = __importStar(require("../abap/2_statements/statements"));
38
+ const Expressions = __importStar(require("../abap/2_statements/expressions"));
39
+ const issue_1 = require("../issue");
40
+ const _basic_rule_config_1 = require("./_basic_rule_config");
41
+ const _abap_rule_1 = require("./_abap_rule");
42
+ const _irule_1 = require("./_irule");
43
+ const edit_helper_1 = require("../edit_helper");
44
+ const severity_1 = require("../severity");
45
+ class IsAssignedInLoopConf extends _basic_rule_config_1.BasicRuleConfig {
46
+ constructor() {
47
+ super(...arguments);
48
+ this.severity = severity_1.Severity.Warning;
49
+ }
50
+ }
51
+ exports.IsAssignedInLoopConf = IsAssignedInLoopConf;
52
+ class IsAssignedInLoop extends _abap_rule_1.ABAPRule {
53
+ constructor() {
54
+ super(...arguments);
55
+ this.conf = new IsAssignedInLoopConf();
56
+ }
57
+ getMetadata() {
58
+ return {
59
+ key: "is_assigned_in_loop",
60
+ title: "IS ASSIGNED after READ TABLE or ASSIGN in a loop",
61
+ shortDescription: `Reports IS [NOT] ASSIGNED after a READ TABLE ... ASSIGNING or ASSIGN inside a loop, without UNASSIGN before it`,
62
+ extendedInformation: `A failed READ TABLE ... ASSIGNING or ASSIGN keeps the previous binding of the field symbol. Inside a
63
+ loop, the field symbol still points to the row of an earlier iteration, so IS ASSIGNED is true
64
+ although the statement failed, and the code goes on with the wrong row.
65
+
66
+ Add UNASSIGN <fs> before the statement, inside the loop, or check sy-subrc after a READ TABLE.
67
+ The quick fix inserts the UNASSIGN; it is not offered for a field symbol declared inline in
68
+ the statement, or for a chained statement.
69
+
70
+ Not reported: a check that also reads sy-subrc, an ASSIGN that cannot fail (a static source
71
+ without dereference, table expression or dynamic part), and loops outside of the
72
+ method, form or function module of the statement. See also subrc_after_assign.`,
73
+ tags: [_irule_1.RuleTag.SingleFile, _irule_1.RuleTag.Quickfix],
74
+ badExample: `LOOP AT lt_cells INTO ls_cell.
75
+ READ TABLE lt_styles ASSIGNING <style> WITH KEY id = ls_cell-style.
76
+ IF <style> IS ASSIGNED.
77
+ ls_cell-format = <style>-format.
78
+ ENDIF.
79
+ ENDLOOP.`,
80
+ goodExample: `LOOP AT lt_cells INTO ls_cell.
81
+ UNASSIGN <style>.
82
+ READ TABLE lt_styles ASSIGNING <style> WITH KEY id = ls_cell-style.
83
+ IF <style> IS ASSIGNED.
84
+ ls_cell-format = <style>-format.
85
+ ENDIF.
86
+ ENDLOOP.`,
87
+ };
88
+ }
89
+ getConfig() {
90
+ return this.conf;
91
+ }
92
+ setConfig(conf) {
93
+ this.conf = conf;
94
+ }
95
+ runParsed(file) {
96
+ const issues = [];
97
+ const statements = file.getStatements();
98
+ for (let i = 0; i < statements.length; i++) {
99
+ const statement = statements[i];
100
+ const name = this.boundByFailable(statement);
101
+ if (name === undefined) {
102
+ continue;
103
+ }
104
+ const fs = name.toUpperCase();
105
+ const loop = this.innermostLoop(i, statements);
106
+ if (loop === undefined || this.unassignedBefore(loop, i, statements, fs) === true) {
107
+ continue;
108
+ }
109
+ const check = this.nextCheck(i, statements, fs);
110
+ if (check === undefined) {
111
+ continue;
112
+ }
113
+ const read = statement.get() instanceof Statements.ReadTable;
114
+ const message = `${read ? "READ TABLE" : "ASSIGN"} in a loop keeps the previous binding of ${name} when it fails, `
115
+ + `UNASSIGN ${name} before it` + (read ? " or check sy-subrc" : "");
116
+ const fix = this.buildFix(file, statement, name);
117
+ issues.push(issue_1.Issue.atStatement(file, check, message, this.getMetadata().key, this.conf.severity, fix));
118
+ }
119
+ return issues;
120
+ }
121
+ ////////////////
122
+ /** the field symbol bound by a READ TABLE ... ASSIGNING or an ASSIGN that can fail */
123
+ boundByFailable(statement) {
124
+ var _a, _b, _c;
125
+ const s = statement.get();
126
+ if (s instanceof Statements.ReadTable) {
127
+ const target = (_a = statement.findDirectExpression(Expressions.ReadTableTarget)) === null || _a === void 0 ? void 0 : _a.findDirectExpression(Expressions.FSTarget);
128
+ return (_b = target === null || target === void 0 ? void 0 : target.findFirstExpression(Expressions.FieldSymbol)) === null || _b === void 0 ? void 0 : _b.concatTokens();
129
+ }
130
+ else if (s instanceof Statements.Assign) {
131
+ const source = statement.findDirectExpression(Expressions.AssignSource);
132
+ if (source === undefined) {
133
+ return undefined;
134
+ }
135
+ const first = source.getFirstToken().getStr().toUpperCase();
136
+ const failable = first === "COMPONENT"
137
+ || first === "TABLE"
138
+ || source.findFirstExpression(Expressions.Dynamic) !== undefined
139
+ || source.findFirstExpression(Expressions.Dereference) !== undefined
140
+ || source.findFirstExpression(Expressions.TableExpression) !== undefined
141
+ || statement.findDirectTokenByText("INCREMENT") !== undefined;
142
+ if (failable === false) {
143
+ return undefined;
144
+ }
145
+ const target = statement.findDirectExpression(Expressions.FSTarget);
146
+ return (_c = target === null || target === void 0 ? void 0 : target.findFirstExpression(Expressions.FieldSymbol)) === null || _c === void 0 ? void 0 : _c.concatTokens();
147
+ }
148
+ return undefined;
149
+ }
150
+ /** index of the innermost LOOP, DO, WHILE or SELECT loop around the statement, in the same block */
151
+ innermostLoop(index, statements) {
152
+ let depth = 0;
153
+ for (let i = index - 1; i >= 0 && this.startsBlock(statements[i]) === false; i--) {
154
+ if (this.endsLoop(statements[i])) {
155
+ depth++;
156
+ }
157
+ else if (this.startsLoop(statements[i])) {
158
+ if (depth === 0) {
159
+ return i;
160
+ }
161
+ depth--;
162
+ }
163
+ }
164
+ return undefined;
165
+ }
166
+ unassignedBefore(loop, index, statements, fs) {
167
+ for (let i = loop + 1; i < index; i++) {
168
+ if (statements[i].get() instanceof Statements.Unassign && this.mentions(statements[i], fs)) {
169
+ return true;
170
+ }
171
+ }
172
+ return false;
173
+ }
174
+ /** the first IS [NOT] ASSIGNED of the field symbol after the statement, before the end of the loop
175
+ * or another UNASSIGN or binding of it; a check that also reads sy-subrc is fine */
176
+ nextCheck(index, statements, fs) {
177
+ let depth = 0;
178
+ for (let i = index + 1; i < statements.length && this.startsBlock(statements[i]) === false; i++) {
179
+ const statement = statements[i];
180
+ if (this.startsLoop(statement)) {
181
+ depth++;
182
+ }
183
+ else if (this.endsLoop(statement)) {
184
+ if (depth === 0) {
185
+ return undefined;
186
+ }
187
+ depth--;
188
+ }
189
+ const concat = statement.concatTokens().toUpperCase();
190
+ if (concat.includes(fs + " IS ASSIGNED") || concat.includes(fs + " IS NOT ASSIGNED")) {
191
+ return concat.includes("SY-SUBRC") ? undefined : statement;
192
+ }
193
+ if ((statement.get() instanceof Statements.Unassign && this.mentions(statement, fs))
194
+ || this.binds(statement, fs)) {
195
+ return undefined;
196
+ }
197
+ }
198
+ return undefined;
199
+ }
200
+ mentions(statement, fs) {
201
+ return statement.findAllExpressions(Expressions.FieldSymbol).some(e => e.concatTokens().toUpperCase() === fs);
202
+ }
203
+ binds(statement, fs) {
204
+ return statement.findAllExpressions(Expressions.FSTarget)
205
+ .some(t => { var _a; return ((_a = t.findFirstExpression(Expressions.FieldSymbol)) === null || _a === void 0 ? void 0 : _a.concatTokens().toUpperCase()) === fs; });
206
+ }
207
+ startsLoop(statement) {
208
+ const s = statement.get();
209
+ return s instanceof Statements.Loop
210
+ || s instanceof Statements.Do
211
+ || s instanceof Statements.While
212
+ || s instanceof Statements.SelectLoop;
213
+ }
214
+ endsLoop(statement) {
215
+ const s = statement.get();
216
+ return s instanceof Statements.EndLoop
217
+ || s instanceof Statements.EndDo
218
+ || s instanceof Statements.EndWhile
219
+ || s instanceof Statements.EndSelect;
220
+ }
221
+ startsBlock(statement) {
222
+ const s = statement.get();
223
+ return s instanceof Statements.MethodImplementation
224
+ || s instanceof Statements.Form
225
+ || s instanceof Statements.FunctionModule
226
+ || s instanceof Statements.Module
227
+ || s instanceof Statements.EndMethod
228
+ || s instanceof Statements.EndForm
229
+ || s instanceof Statements.EndFunction
230
+ || s instanceof Statements.EndModule;
231
+ }
232
+ /** UNASSIGN on a line of its own before the statement, with the same indentation */
233
+ buildFix(file, statement, name) {
234
+ if (statement.getColon() !== undefined || statement.findFirstExpression(Expressions.InlineFS) !== undefined) {
235
+ return undefined;
236
+ }
237
+ const start = statement.getStart();
238
+ return edit_helper_1.EditHelper.insertAt(file, start, `UNASSIGN ${name}.\n` + " ".repeat(start.getCol() - 1));
239
+ }
240
+ }
241
+ exports.IsAssignedInLoop = IsAssignedInLoop;
242
+ //# sourceMappingURL=is_assigned_in_loop.js.map
@@ -144,7 +144,11 @@ ENDIF.
144
144
  && lastChildren[1].getChildren().length === 1
145
145
  && lastChildren[1].getFirstToken().getStr().startsWith("-") === false
146
146
  && lastChildren[2].getFirstToken().getStr() === ")") {
147
- const issue = issue_1.Issue.atToken(file, lastChildren[0].getFirstToken(), "Too many parentheses", this.getMetadata().key, this.conf.severity);
147
+ const fixText = lastChildren[1].concatTokens();
148
+ const start = lastChildren[0].getFirstToken().getStart();
149
+ const end = lastChildren[2].getLastToken().getEnd();
150
+ const fix = edit_helper_1.EditHelper.replaceRange(file, start, end, fixText);
151
+ const issue = issue_1.Issue.atToken(file, lastChildren[0].getFirstToken(), "Too many parentheses", this.getMetadata().key, this.conf.severity, fix);
148
152
  issues.push(issue);
149
153
  }
150
154
  }
@@ -40,6 +40,7 @@ const issue_1 = require("../issue");
40
40
  const _abap_rule_1 = require("./_abap_rule");
41
41
  const _basic_rule_config_1 = require("./_basic_rule_config");
42
42
  const _irule_1 = require("./_irule");
43
+ const edit_helper_1 = require("../edit_helper");
43
44
  class MaxOneMethodParameterPerLineConf extends _basic_rule_config_1.BasicRuleConfig {
44
45
  }
45
46
  exports.MaxOneMethodParameterPerLineConf = MaxOneMethodParameterPerLineConf;
@@ -53,7 +54,7 @@ class MaxOneMethodParameterPerLine extends _abap_rule_1.ABAPRule {
53
54
  key: "max_one_method_parameter_per_line",
54
55
  title: "Max one method parameter definition per line",
55
56
  shortDescription: `Keep max one method parameter description per line`,
56
- tags: [_irule_1.RuleTag.SingleFile, _irule_1.RuleTag.Whitespace],
57
+ tags: [_irule_1.RuleTag.SingleFile, _irule_1.RuleTag.Whitespace, _irule_1.RuleTag.Quickfix],
57
58
  badExample: `
58
59
  METHODS apps_scope_token
59
60
  IMPORTING
@@ -82,7 +83,8 @@ METHODS apps_scope_token
82
83
  continue;
83
84
  }
84
85
  if (prev.getFirstToken().getStart().getRow() === p.getFirstToken().getStart().getRow()) {
85
- const issue = issue_1.Issue.atToken(file, prev.getFirstToken(), this.getMetadata().title, this.getMetadata().key, this.conf.severity);
86
+ const fix = this.buildFix(file, statement, p);
87
+ const issue = issue_1.Issue.atToken(file, prev.getFirstToken(), this.getMetadata().title, this.getMetadata().key, this.conf.severity, fix);
86
88
  issues.push(issue);
87
89
  }
88
90
  prev = p;
@@ -90,6 +92,30 @@ METHODS apps_scope_token
90
92
  }
91
93
  return issues;
92
94
  }
95
+ /** moves the parameter, or its preceding section keyword, to a new line */
96
+ buildFix(file, statement, param) {
97
+ const tokens = statement.getTokens();
98
+ const index = tokens.indexOf(param.getFirstToken());
99
+ if (index < 1) {
100
+ return undefined;
101
+ }
102
+ let target = tokens[index];
103
+ let indent = statement.findFirstExpression(Expressions.MethodParam).getFirstToken().getStart().getCol();
104
+ const before = tokens[index - 1];
105
+ if (MaxOneMethodParameterPerLine.SECTIONS.includes(before.getStr().toUpperCase())) {
106
+ if (index < 2) {
107
+ return undefined;
108
+ }
109
+ target = before;
110
+ indent = tokens.find(t => MaxOneMethodParameterPerLine.SECTIONS.includes(t.getStr().toUpperCase())).getStart().getCol();
111
+ }
112
+ const previous = tokens[tokens.indexOf(target) - 1];
113
+ if (previous.getEnd().getRow() !== target.getStart().getRow()) {
114
+ return undefined;
115
+ }
116
+ return edit_helper_1.EditHelper.replaceRange(file, previous.getEnd(), target.getStart(), "\n" + " ".repeat(indent - 1));
117
+ }
93
118
  }
94
119
  exports.MaxOneMethodParameterPerLine = MaxOneMethodParameterPerLine;
120
+ MaxOneMethodParameterPerLine.SECTIONS = ["IMPORTING", "EXPORTING", "CHANGING", "RETURNING"];
95
121
  //# sourceMappingURL=max_one_method_parameter_per_line.js.map
@@ -40,6 +40,8 @@ const _abap_rule_1 = require("./_abap_rule");
40
40
  const sequential_blank_1 = require("./sequential_blank");
41
41
  const Statements = __importStar(require("../abap/2_statements/statements"));
42
42
  const _irule_1 = require("./_irule");
43
+ const edit_helper_1 = require("../edit_helper");
44
+ const position_1 = require("../position");
43
45
  var NewlineLogic;
44
46
  (function (NewlineLogic) {
45
47
  NewlineLogic["Exact"] = "exact";
@@ -69,7 +71,7 @@ class NewlineBetweenMethods extends _abap_rule_1.ABAPRule {
69
71
  key: "newline_between_methods",
70
72
  title: "New line between methods",
71
73
  shortDescription: `Checks for newlines between method implementations.`,
72
- tags: [_irule_1.RuleTag.Whitespace, _irule_1.RuleTag.SingleFile],
74
+ tags: [_irule_1.RuleTag.Whitespace, _irule_1.RuleTag.Quickfix, _irule_1.RuleTag.SingleFile],
73
75
  };
74
76
  }
75
77
  getMessage() {
@@ -98,18 +100,35 @@ class NewlineBetweenMethods extends _abap_rule_1.ABAPRule {
98
100
  continue;
99
101
  }
100
102
  let counter = 0;
101
- while (sequential_blank_1.SequentialBlank.isBlankOrWhitespace(rows[nextRow]) && (counter <= this.conf.count + 1)) {
103
+ while (sequential_blank_1.SequentialBlank.isBlankOrWhitespace(rows[nextRow])) {
102
104
  counter++;
103
105
  nextRow++;
104
106
  }
105
107
  if ((counter !== this.conf.count && this.conf.logic === NewlineLogic.Exact)
106
108
  || (counter >= this.conf.count && this.conf.logic === NewlineLogic.Less)
107
109
  || counter === 0) {
108
- issues.push(issue_1.Issue.atStatement(file, statement, this.getMessage(), this.getMetadata().key, this.conf.severity));
110
+ const fix = this.buildFix(file, rows, statement.getStart().getRow(), counter);
111
+ issues.push(issue_1.Issue.atStatement(file, statement, this.getMessage(), this.getMetadata().key, this.conf.severity, fix));
109
112
  }
110
113
  }
111
114
  return issues;
112
115
  }
116
+ buildFix(file, rows, row, blanks) {
117
+ let target = this.conf.count;
118
+ if (this.conf.logic === NewlineLogic.Less) {
119
+ target = blanks === 0 ? 1 : this.conf.count - 1;
120
+ }
121
+ if (blanks < target) {
122
+ const end = new position_1.Position(row, rows[row - 1].length + 1);
123
+ return edit_helper_1.EditHelper.insertAt(file, end, "\n".repeat(target - blanks));
124
+ }
125
+ else if (blanks > target) {
126
+ const start = new position_1.Position(row + target, rows[row + target - 1].length + 1);
127
+ const end = new position_1.Position(row + blanks, rows[row + blanks - 1].length + 1);
128
+ return edit_helper_1.EditHelper.deleteRange(file, start, end);
129
+ }
130
+ return undefined;
131
+ }
113
132
  isConfigurationValid() {
114
133
  if (this.conf.count < 1 || (this.conf.count === 1 && this.conf.logic === NewlineLogic.Less)) {
115
134
  return false;
@@ -39,7 +39,7 @@ class PreferPragmas extends _abap_rule_1.ABAPRule {
39
39
  return {
40
40
  key: "prefer_pragmas",
41
41
  title: "prefer pragmas over pseudo comments ",
42
- shortDescription: `prefer pragmas over pseudo comments `,
42
+ shortDescription: `prefer pragmas over pseudo comments`,
43
43
  extendedInformation: `https://github.com/SAP/styleguides/blob/main/clean-abap/CleanABAP.md#prefer-pragmas-to-pseudo-comments`,
44
44
  tags: [_irule_1.RuleTag.SingleFile, _irule_1.RuleTag.Styleguide],
45
45
  badExample: `DATA foo1 TYPE i. "#EC NEEDED`,
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.RedundantCast = exports.RedundantCastConf = void 0;
37
+ const Expressions = __importStar(require("../abap/2_statements/expressions"));
38
+ const _reference_1 = require("../abap/5_syntax/_reference");
39
+ const syntax_1 = require("../abap/5_syntax/syntax");
40
+ const nodes_1 = require("../abap/nodes");
41
+ const _typed_identifier_1 = require("../abap/types/_typed_identifier");
42
+ const basic_1 = require("../abap/types/basic");
43
+ const issue_1 = require("../issue");
44
+ const _abap_object_1 = require("../objects/_abap_object");
45
+ const _basic_rule_config_1 = require("./_basic_rule_config");
46
+ const _irule_1 = require("./_irule");
47
+ const edit_helper_1 = require("../edit_helper");
48
+ const version_1 = require("../version");
49
+ class RedundantCastConf extends _basic_rule_config_1.BasicRuleConfig {
50
+ }
51
+ exports.RedundantCastConf = RedundantCastConf;
52
+ class RedundantCast {
53
+ constructor() {
54
+ this.conf = new RedundantCastConf();
55
+ }
56
+ getMetadata() {
57
+ return {
58
+ key: "redundant_cast",
59
+ title: "Redundant Cast",
60
+ shortDescription: `Find redundant CAST expressions`,
61
+ extendedInformation: `Reports CAST expressions whose operand is already a reference to the cast's target class or interface.`,
62
+ tags: [_irule_1.RuleTag.Quickfix],
63
+ badExample: `DATA ref TYPE REF TO lcl.
64
+ DATA(copy) = CAST lcl( ref ).`,
65
+ goodExample: `DATA ref TYPE REF TO lcl.
66
+ DATA(copy) = ref.`,
67
+ };
68
+ }
69
+ getConfig() {
70
+ return this.conf;
71
+ }
72
+ setConfig(conf) {
73
+ this.conf = conf;
74
+ }
75
+ initialize(reg) {
76
+ this.reg = reg;
77
+ return this;
78
+ }
79
+ run(obj) {
80
+ var _a;
81
+ if (!(0, version_1.releaseAtLeast)(this.reg.getConfig().getRelease(), version_1.Release.v740sp02)
82
+ && !this.reg.getConfig().isOpenABAP()) {
83
+ return [];
84
+ }
85
+ if (!(obj instanceof _abap_object_1.ABAPObject)) {
86
+ return [];
87
+ }
88
+ const syntax = new syntax_1.SyntaxLogic(this.reg, obj).run();
89
+ if (syntax.issues.length > 0) {
90
+ return [];
91
+ }
92
+ const issues = [];
93
+ for (const file of obj.getABAPFiles()) {
94
+ const structure = file.getStructure();
95
+ if (structure === undefined) {
96
+ continue;
97
+ }
98
+ for (const cast of structure.findAllExpressionsRecursive(Expressions.Cast)) {
99
+ const typeExpression = cast.findDirectExpression(Expressions.TypeNameOrInfer);
100
+ const source = cast.findDirectExpression(Expressions.Source);
101
+ if (typeExpression === undefined
102
+ || source === undefined
103
+ || cast.findDirectExpression(Expressions.Let) !== undefined) {
104
+ continue;
105
+ }
106
+ const scope = syntax.spaghetti.lookupPosition(cast.getFirstToken().getStart(), file.getFilename());
107
+ if (scope === undefined) {
108
+ continue;
109
+ }
110
+ const target = (_a = scope.getData().references.find(reference => reference.referenceType === _reference_1.ReferenceType.InferredType
111
+ && reference.position.getStart().equals(typeExpression.getFirstToken().getStart()))) === null || _a === void 0 ? void 0 : _a.resolved;
112
+ if (!(target instanceof _typed_identifier_1.TypedIdentifier)) {
113
+ continue;
114
+ }
115
+ const operandType = this.sourceType(source, scope);
116
+ if (operandType === undefined || this.sameType(target.getType(), operandType) === false) {
117
+ continue;
118
+ }
119
+ const closingParen = cast.getDirectTokens().find(token => token.getStr() === ")" && token.getStart().isAfter(source.getLastToken().getStart()));
120
+ if (closingParen === undefined) {
121
+ continue;
122
+ }
123
+ const fix = edit_helper_1.EditHelper.replaceRange(file, cast.getFirstToken().getStart(), closingParen.getEnd(), source.concatTokens());
124
+ issues.push(issue_1.Issue.atRange(file, cast.getFirstToken().getStart(), closingParen.getEnd(), "Redundant CAST expression", this.getMetadata().key, this.conf.severity, fix));
125
+ }
126
+ }
127
+ return issues;
128
+ }
129
+ /** The type of a plain variable or attribute, undefined for anything more complex */
130
+ sourceType(source, scope) {
131
+ var _a;
132
+ const children = source.getChildren();
133
+ if (children.length !== 1 || !(children[0].get() instanceof Expressions.FieldChain)) {
134
+ return undefined;
135
+ }
136
+ const fieldChain = children[0];
137
+ const last = fieldChain.getLastChild();
138
+ if (!(last instanceof nodes_1.ExpressionNode)
139
+ || !(last.get() instanceof Expressions.SourceField || last.get() instanceof Expressions.AttributeName)) {
140
+ return undefined;
141
+ }
142
+ const operand = (_a = scope.getData().references.find(reference => reference.referenceType === _reference_1.ReferenceType.DataReadReference
143
+ && reference.position.getStart().equals(last.getFirstToken().getStart()))) === null || _a === void 0 ? void 0 : _a.resolved;
144
+ if (!(operand instanceof _typed_identifier_1.TypedIdentifier)) {
145
+ return undefined;
146
+ }
147
+ return operand.getType();
148
+ }
149
+ sameType(first, second) {
150
+ if (!(first instanceof basic_1.ObjectReferenceType) || !(second instanceof basic_1.ObjectReferenceType)) {
151
+ return false;
152
+ }
153
+ return first.getIdentifierName().toUpperCase() === second.getIdentifierName().toUpperCase();
154
+ }
155
+ }
156
+ exports.RedundantCast = RedundantCast;
157
+ //# sourceMappingURL=redundant_cast.js.map
@@ -196,9 +196,9 @@ text = text.`,
196
196
  context = this.infer(context, this.sourceType(child, scope));
197
197
  }
198
198
  else if (child.get() instanceof Expressions.ArithOperator) {
199
- if (child.concatTokens() === "**") {
200
- context = new basic_1.FloatType();
201
- }
199
+ // the CONV sets the calculation type of arithmetic expressions, only redundant
200
+ // if the assignment target has the same type, see arithmeticUsesSameTargetType
201
+ return undefined;
202
202
  }
203
203
  else {
204
204
  return undefined;
@@ -9,7 +9,6 @@ const position_1 = require("../position");
9
9
  const issue_1 = require("../issue");
10
10
  const _abap_object_1 = require("../objects/_abap_object");
11
11
  const severity_1 = require("../severity");
12
- // todo, check for cycles/circular dependencies, method findTop
13
12
  // todo, add configurable error for multiple use includes
14
13
  const FMXXINCLUDE = /^(\/\w+\/)?L.+XX$/;
15
14
  function getABAPObjects(reg) {
@@ -45,11 +44,33 @@ class Graph {
45
44
  }
46
45
  this.edges[from.filename].push(toFilename);
47
46
  }
48
- findTop(filename) {
47
+ // true if "to" is reachable from "from" via the edges, "from" itself counts
48
+ isReachable(from, to) {
49
+ const seen = new Set();
50
+ const stack = [from];
51
+ while (stack.length > 0) {
52
+ const current = stack.pop();
53
+ if (current === to) {
54
+ return true;
55
+ }
56
+ if (seen.has(current) === false) {
57
+ seen.add(current);
58
+ stack.push(...this.edges[current] || []);
59
+ }
60
+ }
61
+ return false;
62
+ }
63
+ findTop(filename, path = new Set()) {
49
64
  const ret = [];
65
+ path.add(filename);
50
66
  for (const to of this.edges[filename] || []) {
51
- ret.push(...this.findTop(to));
67
+ // an INCLUDE cycle, the file is already on the current path
68
+ if (path.has(to)) {
69
+ continue;
70
+ }
71
+ ret.push(...this.findTop(to, path));
52
72
  }
73
+ path.delete(filename);
53
74
  if (ret.length === 0) {
54
75
  const found = this.findVertexByFilename(filename);
55
76
  if (found !== undefined) {
@@ -104,6 +125,7 @@ class IncludeGraph {
104
125
  ///////////////////////////////
105
126
  build() {
106
127
  this.addVertices();
128
+ const included = [];
107
129
  for (const o of getABAPObjects(this.reg)) {
108
130
  for (const f of o.getABAPFiles()) {
109
131
  if (f.getFilename().includes(".prog.screen_") || f.getFilename().includes(".fugr.screen_")) {
@@ -134,11 +156,19 @@ class IncludeGraph {
134
156
  }
135
157
  else {
136
158
  this.graph.addEdge(found, f.getFilename());
159
+ included.push({ file: f, statement: s, vertex: found, name });
137
160
  }
138
161
  }
139
162
  }
140
163
  }
141
164
  }
165
+ // cycles are not followed, but ABAP rejects them, so report them
166
+ for (const i of included) {
167
+ if (this.graph.isReachable(i.file.getFilename(), i.vertex.filename)) {
168
+ const issue = issue_1.Issue.atStatement(i.file, i.statement, "Cyclic INCLUDE, " + i.name, new check_include_1.CheckInclude().getMetadata().key, this.severity);
169
+ this.issues.push(issue);
170
+ }
171
+ }
142
172
  }
143
173
  addVertices() {
144
174
  for (const o of getABAPObjects(this.reg)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abaplint/core",
3
- "version": "2.120.77",
3
+ "version": "2.120.79",
4
4
  "description": "abaplint - Core API",
5
5
  "main": "build/src/index.js",
6
6
  "typings": "build/abaplint.d.ts",