@abaplint/core 2.120.26 → 2.120.28
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.
- package/build/abaplint.d.ts +4 -0
- package/build/src/abap/2_statements/expressions/sql_group_by.js +1 -1
- package/build/src/abap/5_syntax/expressions/_check_database_fields.js +1 -1
- package/build/src/abap/5_syntax/expressions/select.js +65 -7
- package/build/src/abap/5_syntax/expressions/value_body.js +30 -10
- package/build/src/objects/data_element.js +17 -0
- package/build/src/objects/domain.js +5 -0
- package/build/src/registry.js +1 -1
- package/build/src/rules/cds_check_syntax.js +299 -0
- package/build/src/rules/index.js +1 -0
- package/package.json +1 -1
package/build/abaplint.d.ts
CHANGED
|
@@ -1790,6 +1790,9 @@ declare class DataElement extends AbstractObject {
|
|
|
1790
1790
|
};
|
|
1791
1791
|
setDirty(): void;
|
|
1792
1792
|
getDomainName(): string | undefined;
|
|
1793
|
+
/** parseType() cannot distinguish DEC, CURR, and QUAN because they all become PackedType.
|
|
1794
|
+
* Return the original DDIC datatype instead, resolving domain-backed data elements when possible. */
|
|
1795
|
+
getDataType(reg?: IRegistry): string | undefined;
|
|
1793
1796
|
getTexts(): {
|
|
1794
1797
|
short?: string;
|
|
1795
1798
|
medium?: string;
|
|
@@ -2053,6 +2056,7 @@ declare class Domain extends AbstractObject {
|
|
|
2053
2056
|
getType(): string;
|
|
2054
2057
|
getDescription(): string | undefined;
|
|
2055
2058
|
getConversionExit(): string | undefined;
|
|
2059
|
+
getDataType(): string | undefined;
|
|
2056
2060
|
getAllowedNaming(): {
|
|
2057
2061
|
maxLength: number;
|
|
2058
2062
|
allowNamespace: boolean;
|
|
@@ -9,7 +9,7 @@ const sql_field_1 = require("./sql_field");
|
|
|
9
9
|
const sql_grouping_sets_1 = require("./sql_grouping_sets");
|
|
10
10
|
class SQLGroupBy extends combi_1.Expression {
|
|
11
11
|
getRunnable() {
|
|
12
|
-
const fieldName = (0, combi_1.alt)(sql_field_name_1.SQLFieldName, dynamic_1.Dynamic);
|
|
12
|
+
const fieldName = (0, combi_1.seq)((0, combi_1.stopBefore)("ORDER", "BY"), (0, combi_1.alt)(sql_field_name_1.SQLFieldName, dynamic_1.Dynamic));
|
|
13
13
|
const expr = (0, combi_1.ver)(version_1.Release.v740sp02, sql_field_1.SQLField, { also: combi_1.AlsoIn.OpenABAP });
|
|
14
14
|
const newGroup = (0, combi_1.ver)(version_1.Release.v740sp02, (0, combi_1.seq)("GROUP BY", expr, (0, combi_1.altPrio)((0, combi_1.plus)((0, combi_1.seq)(",", expr)), (0, combi_1.optPrio)((0, combi_1.plus)(fieldName)))), { also: combi_1.AlsoIn.OpenABAP });
|
|
15
15
|
const old = (0, combi_1.seq)((0, combi_1.plus)((0, combi_1.seq)(fieldName, ",")), fieldName);
|
|
@@ -25,7 +25,7 @@ function checkDatabaseFields(fields, dbSources, input, token) {
|
|
|
25
25
|
if (field === "*") {
|
|
26
26
|
continue;
|
|
27
27
|
}
|
|
28
|
-
if (
|
|
28
|
+
if (/^[A-Z_]\w*$/i.test(field) && type.getComponentByName(field) === undefined) {
|
|
29
29
|
const message = `checkFields, field ${field} not found`;
|
|
30
30
|
input.issues.push((0, _syntax_input_1.syntaxIssue)(input, token, message));
|
|
31
31
|
return;
|
|
@@ -74,7 +74,7 @@ class Select {
|
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
76
|
(0, _check_database_fields_1.checkDatabaseFields)(fields.map(field => field.code), dbSources, input, node.getFirstToken());
|
|
77
|
-
const intoExpression = this.handleInto(node, input, fields, dbSources);
|
|
77
|
+
const intoExpression = this.handleInto(node, input, fields, dbSources, from);
|
|
78
78
|
const fae = node.findDirectExpression(Expressions.SQLForAllEntries);
|
|
79
79
|
if (fae) {
|
|
80
80
|
input.scope.push(_scope_type_1.ScopeType.OpenSQL, "SELECT", token.getStart(), input.filename);
|
|
@@ -235,12 +235,12 @@ class Select {
|
|
|
235
235
|
input.issues.push((0, _syntax_input_1.syntaxIssue)(input, node.getFirstToken(), message));
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
|
-
static handleInto(node, input, fields, dbSources) {
|
|
238
|
+
static handleInto(node, input, fields, dbSources, from) {
|
|
239
239
|
const intoTable = node.findDirectExpression(Expressions.SQLIntoTable);
|
|
240
240
|
if (intoTable) {
|
|
241
241
|
const inline = intoTable.findFirstExpression(Expressions.InlineData);
|
|
242
242
|
if (inline) {
|
|
243
|
-
inline_data_1.InlineData.runSyntax(inline, input, this.buildTableType(fields, dbSources, input.scope));
|
|
243
|
+
inline_data_1.InlineData.runSyntax(inline, input, this.buildTableType(fields, dbSources, input.scope, from));
|
|
244
244
|
}
|
|
245
245
|
return intoTable;
|
|
246
246
|
}
|
|
@@ -388,11 +388,36 @@ class Select {
|
|
|
388
388
|
return basic_1.VoidType.get("SELECT_todo13");
|
|
389
389
|
}
|
|
390
390
|
}
|
|
391
|
-
static buildTableType(fields, dbSources, scope) {
|
|
392
|
-
|
|
391
|
+
static buildTableType(fields, dbSources, scope, from) {
|
|
392
|
+
var _a;
|
|
393
|
+
const tableOptions = { withHeader: false, keyType: basic_1.TableKeyType.empty };
|
|
394
|
+
if (dbSources.length > 1) {
|
|
395
|
+
const sourceInfos = this.buildDatabaseSourceInfos(from, dbSources);
|
|
396
|
+
if (fields.length === 1 && fields[0].code === "*") {
|
|
397
|
+
const components = [];
|
|
398
|
+
for (const info of sourceInfos) {
|
|
399
|
+
const type = (_a = info.source) === null || _a === void 0 ? void 0 : _a.parseType(scope.getRegistry());
|
|
400
|
+
if (!(type instanceof basic_1.StructureType)) {
|
|
401
|
+
return basic_1.VoidType.get("SELECT_todo3");
|
|
402
|
+
}
|
|
403
|
+
components.push({ name: info.alias || info.name, type });
|
|
404
|
+
}
|
|
405
|
+
return new basic_1.TableType(new basic_1.StructureType(components), tableOptions, undefined);
|
|
406
|
+
}
|
|
407
|
+
const components = [];
|
|
408
|
+
for (const field of fields) {
|
|
409
|
+
const type = this.findFieldType(field.code, sourceInfos, scope);
|
|
410
|
+
if (type === undefined) {
|
|
411
|
+
return basic_1.VoidType.get("SELECT_todo3");
|
|
412
|
+
}
|
|
413
|
+
const name = field.as || field.code.split("~").pop();
|
|
414
|
+
components.push({ name, type });
|
|
415
|
+
}
|
|
416
|
+
return new basic_1.TableType(new basic_1.StructureType(components), tableOptions, undefined);
|
|
417
|
+
}
|
|
418
|
+
else if (dbSources.length !== 1) {
|
|
393
419
|
return basic_1.VoidType.get("SELECT_todo3");
|
|
394
420
|
}
|
|
395
|
-
const tableOptions = { withHeader: false, keyType: basic_1.TableKeyType.empty };
|
|
396
421
|
if (dbSources[0] === undefined) {
|
|
397
422
|
// then its a voided table
|
|
398
423
|
return new basic_1.TableType(basic_1.VoidType.get("SELECT_todo4"), tableOptions, undefined);
|
|
@@ -418,6 +443,39 @@ class Select {
|
|
|
418
443
|
}
|
|
419
444
|
return basic_1.VoidType.get("SELECT_todo7");
|
|
420
445
|
}
|
|
446
|
+
static buildDatabaseSourceInfos(from, dbSources) {
|
|
447
|
+
const fromSources = from.findAllExpressions(Expressions.SQLFromSource);
|
|
448
|
+
return dbSources.map((source, index) => {
|
|
449
|
+
var _a, _b;
|
|
450
|
+
const fromSource = fromSources[index];
|
|
451
|
+
const name = ((_a = fromSource === null || fromSource === void 0 ? void 0 : fromSource.findFirstExpression(Expressions.DatabaseTable)) === null || _a === void 0 ? void 0 : _a.getFirstToken().getStr().replace(/^\*/, "").toUpperCase()) || "";
|
|
452
|
+
const alias = ((_b = fromSource === null || fromSource === void 0 ? void 0 : fromSource.findDirectExpression(Expressions.SQLAsName)) === null || _b === void 0 ? void 0 : _b.concatTokens().toUpperCase()) || "";
|
|
453
|
+
return { source, name, alias };
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
static findFieldType(code, sourceInfos, scope) {
|
|
457
|
+
var _a;
|
|
458
|
+
const separator = code.indexOf("~");
|
|
459
|
+
const qualifier = separator >= 0 ? code.substring(0, separator) : undefined;
|
|
460
|
+
const fieldName = separator >= 0 ? code.substring(separator + 1) : code;
|
|
461
|
+
const candidates = qualifier === undefined
|
|
462
|
+
? sourceInfos
|
|
463
|
+
: sourceInfos.filter(info => info.alias === qualifier || info.name === qualifier);
|
|
464
|
+
for (const candidate of candidates) {
|
|
465
|
+
let type = (_a = candidate.source) === null || _a === void 0 ? void 0 : _a.parseType(scope.getRegistry());
|
|
466
|
+
for (const component of fieldName.split("-")) {
|
|
467
|
+
if (!(type instanceof basic_1.StructureType)) {
|
|
468
|
+
type = undefined;
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
type = type.getComponentByName(component);
|
|
472
|
+
}
|
|
473
|
+
if (type) {
|
|
474
|
+
return type;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
421
479
|
static findFields(node, input) {
|
|
422
480
|
var _a, _b;
|
|
423
481
|
let expr = undefined;
|
|
@@ -431,7 +489,7 @@ class Select {
|
|
|
431
489
|
let code = field.concatTokens().toUpperCase();
|
|
432
490
|
const as = ((_b = field.findDirectExpression(Expressions.SQLAsName)) === null || _b === void 0 ? void 0 : _b.concatTokens()) || "";
|
|
433
491
|
if (as !== "") {
|
|
434
|
-
code = code.replace(" AS " + as, "");
|
|
492
|
+
code = code.replace(" AS " + as.toUpperCase(), "");
|
|
435
493
|
}
|
|
436
494
|
ret.push({ code, as, expression: field });
|
|
437
495
|
}
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.ValueBody = void 0;
|
|
37
|
+
const nodes_1 = require("../../nodes");
|
|
37
38
|
const Expressions = __importStar(require("../../2_statements/expressions"));
|
|
38
39
|
const for_1 = require("./for");
|
|
39
40
|
const source_1 = require("./source");
|
|
@@ -43,7 +44,7 @@ const basic_1 = require("../../types/basic");
|
|
|
43
44
|
const _syntax_input_1 = require("../_syntax_input");
|
|
44
45
|
class ValueBody {
|
|
45
46
|
static runSyntax(node, input, targetType) {
|
|
46
|
-
var _a, _b;
|
|
47
|
+
var _a, _b, _c;
|
|
47
48
|
if (node === undefined) {
|
|
48
49
|
return targetType;
|
|
49
50
|
}
|
|
@@ -60,26 +61,45 @@ class ValueBody {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
const fields = new Set();
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
const hasTableLines = node.findDirectExpression(Expressions.ValueBodyLine) !== undefined;
|
|
65
|
+
for (const child of node.getChildren()) {
|
|
66
|
+
if (!(child instanceof nodes_1.ExpressionNode)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
else if (child.get() instanceof Expressions.FieldAssignment) {
|
|
70
|
+
const fieldname = (_a = child.findDirectExpression(Expressions.FieldSub)) === null || _a === void 0 ? void 0 : _a.concatTokens().toUpperCase();
|
|
71
|
+
if (fieldname && hasTableLines === false && fields.has(fieldname)) {
|
|
72
|
+
const message = "Duplicate field assignment";
|
|
73
|
+
input.issues.push((0, _syntax_input_1.syntaxIssue)(input, child.getFirstToken(), message));
|
|
74
|
+
return basic_1.VoidType.get(_syntax_input_1.CheckSyntaxKey);
|
|
75
|
+
}
|
|
68
76
|
if (fieldname) {
|
|
69
|
-
|
|
77
|
+
fields.add(fieldname);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (child.get() instanceof Expressions.ValueBodyLine) {
|
|
81
|
+
const rowFields = new Set(fields);
|
|
82
|
+
for (const assignment of child.findDirectExpressions(Expressions.FieldAssignment)) {
|
|
83
|
+
const fieldname = (_b = assignment.findDirectExpression(Expressions.FieldSub)) === null || _b === void 0 ? void 0 : _b.concatTokens().toUpperCase();
|
|
84
|
+
if (fieldname && rowFields.has(fieldname)) {
|
|
70
85
|
const message = "Duplicate field assignment";
|
|
71
|
-
input.issues.push((0, _syntax_input_1.syntaxIssue)(input,
|
|
86
|
+
input.issues.push((0, _syntax_input_1.syntaxIssue)(input, assignment.getFirstToken(), message));
|
|
72
87
|
return basic_1.VoidType.get(_syntax_input_1.CheckSyntaxKey);
|
|
73
88
|
}
|
|
74
|
-
|
|
89
|
+
if (fieldname) {
|
|
90
|
+
rowFields.add(fieldname);
|
|
91
|
+
}
|
|
75
92
|
}
|
|
76
93
|
}
|
|
77
94
|
}
|
|
95
|
+
for (const s of node.findDirectExpressions(Expressions.FieldAssignment)) {
|
|
96
|
+
field_assignment_1.FieldAssignment.runSyntax(s, input, targetType);
|
|
97
|
+
}
|
|
78
98
|
let type = undefined; // todo, this is only correct if there is a single source in the body
|
|
79
99
|
for (const s of node.findDirectExpressions(Expressions.Source)) {
|
|
80
100
|
type = source_1.Source.runSyntax(s, input, type);
|
|
81
101
|
}
|
|
82
|
-
for (const s of ((
|
|
102
|
+
for (const s of ((_c = node.findDirectExpression(Expressions.ValueBase)) === null || _c === void 0 ? void 0 : _c.findDirectExpressions(Expressions.Source)) || []) {
|
|
83
103
|
type = source_1.Source.runSyntax(s, input, type);
|
|
84
104
|
}
|
|
85
105
|
for (const foo of node.findDirectExpressions(Expressions.ValueBodyLine)) {
|
|
@@ -38,6 +38,7 @@ const _abstract_object_1 = require("./_abstract_object");
|
|
|
38
38
|
const ddic_1 = require("../ddic");
|
|
39
39
|
const Types = __importStar(require("../abap/types/basic"));
|
|
40
40
|
const xml_utils_1 = require("../xml_utils");
|
|
41
|
+
const domain_1 = require("./domain");
|
|
41
42
|
class DataElement extends _abstract_object_1.AbstractObject {
|
|
42
43
|
constructor() {
|
|
43
44
|
super(...arguments);
|
|
@@ -68,6 +69,22 @@ class DataElement extends _abstract_object_1.AbstractObject {
|
|
|
68
69
|
this.parse();
|
|
69
70
|
return (_a = this.parsedXML) === null || _a === void 0 ? void 0 : _a.domname;
|
|
70
71
|
}
|
|
72
|
+
/** parseType() cannot distinguish DEC, CURR, and QUAN because they all become PackedType.
|
|
73
|
+
* Return the original DDIC datatype instead, resolving domain-backed data elements when possible. */
|
|
74
|
+
getDataType(reg) {
|
|
75
|
+
var _a, _b;
|
|
76
|
+
this.parse();
|
|
77
|
+
if ((_a = this.parsedXML) === null || _a === void 0 ? void 0 : _a.datatype) {
|
|
78
|
+
return this.parsedXML.datatype;
|
|
79
|
+
}
|
|
80
|
+
if (reg && ((_b = this.parsedXML) === null || _b === void 0 ? void 0 : _b.refkind) === "D" && this.parsedXML.domname) {
|
|
81
|
+
const domain = reg.getObject("DOMA", this.parsedXML.domname);
|
|
82
|
+
if (domain instanceof domain_1.Domain) {
|
|
83
|
+
return domain.getDataType();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
71
88
|
getTexts() {
|
|
72
89
|
var _a;
|
|
73
90
|
this.parse();
|
|
@@ -50,6 +50,11 @@ class Domain extends _abstract_object_1.AbstractObject {
|
|
|
50
50
|
var _a;
|
|
51
51
|
return (_a = this.parsedXML) === null || _a === void 0 ? void 0 : _a.conversionExit;
|
|
52
52
|
}
|
|
53
|
+
getDataType() {
|
|
54
|
+
var _a;
|
|
55
|
+
this.parse();
|
|
56
|
+
return (_a = this.parsedXML) === null || _a === void 0 ? void 0 : _a.datatype;
|
|
57
|
+
}
|
|
53
58
|
getAllowedNaming() {
|
|
54
59
|
return {
|
|
55
60
|
maxLength: 30,
|
package/build/src/registry.js
CHANGED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CDSCheckSyntax = exports.CDSCheckSyntaxConf = void 0;
|
|
4
|
+
const nodes_1 = require("../abap/nodes");
|
|
5
|
+
const basic_1 = require("../abap/types/basic");
|
|
6
|
+
const expressions_1 = require("../cds/expressions");
|
|
7
|
+
const ddic_1 = require("../ddic");
|
|
8
|
+
const issue_1 = require("../issue");
|
|
9
|
+
const objects_1 = require("../objects");
|
|
10
|
+
const _basic_rule_config_1 = require("./_basic_rule_config");
|
|
11
|
+
const _irule_1 = require("./_irule");
|
|
12
|
+
class CDSCheckSyntaxConf extends _basic_rule_config_1.BasicRuleConfig {
|
|
13
|
+
}
|
|
14
|
+
exports.CDSCheckSyntaxConf = CDSCheckSyntaxConf;
|
|
15
|
+
const KNOWN_QUAN_DATA_ELEMENTS = ["MENGE_D"];
|
|
16
|
+
const KNOWN_CURR_DATA_ELEMENTS = ["BWERT", "DZWERT"];
|
|
17
|
+
class CDSCheckSyntax {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.conf = new CDSCheckSyntaxConf();
|
|
20
|
+
}
|
|
21
|
+
getMetadata() {
|
|
22
|
+
return {
|
|
23
|
+
key: "cds_check_syntax",
|
|
24
|
+
title: "CDS Check Syntax",
|
|
25
|
+
shortDescription: "Checks CDS source, field, type, and semantic annotation references",
|
|
26
|
+
extendedInformation: `Performs semantic checks which require other DDIC objects to be available.
|
|
27
|
+
|
|
28
|
+
Missing objects are only reported when their names match the configured errorNamespace.`,
|
|
29
|
+
tags: [_irule_1.RuleTag.Syntax],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
getConfig() {
|
|
33
|
+
return this.conf;
|
|
34
|
+
}
|
|
35
|
+
setConfig(conf) {
|
|
36
|
+
this.conf = conf;
|
|
37
|
+
}
|
|
38
|
+
initialize(reg) {
|
|
39
|
+
this.reg = reg;
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
run(object) {
|
|
43
|
+
if (!(object instanceof objects_1.DataDefinition)) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
const tree = object.getTree();
|
|
47
|
+
const file = object.findSourceFile();
|
|
48
|
+
if (tree === undefined || file === undefined) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const issues = [];
|
|
52
|
+
const ddic = new ddic_1.DDIC(this.reg);
|
|
53
|
+
const sources = this.findSources(tree, ddic, issues, file);
|
|
54
|
+
this.checkRelations(tree, ddic, issues, file);
|
|
55
|
+
this.checkTypes(tree, ddic, issues, file);
|
|
56
|
+
this.checkRootProjection(tree, ddic, issues, file);
|
|
57
|
+
this.checkFieldAnnotations(tree, ddic, issues, file);
|
|
58
|
+
this.checkFields(tree, sources, object, issues, file);
|
|
59
|
+
return issues;
|
|
60
|
+
}
|
|
61
|
+
findSources(tree, ddic, issues, file) {
|
|
62
|
+
var _a;
|
|
63
|
+
const sources = [];
|
|
64
|
+
for (const node of tree.findAllExpressionsRecursive(expressions_1.CDSSource)) {
|
|
65
|
+
const prefixed = node.findDirectExpression(expressions_1.CDSPrefixedName);
|
|
66
|
+
const entityNode = prefixed === null || prefixed === void 0 ? void 0 : prefixed.findDirectExpression(expressions_1.CDSName);
|
|
67
|
+
if (entityNode === undefined) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const entity = this.name(entityNode);
|
|
71
|
+
const asName = (_a = node.findDirectExpression(expressions_1.CDSAs)) === null || _a === void 0 ? void 0 : _a.findDirectExpression(expressions_1.CDSName);
|
|
72
|
+
const bareAlias = node.findDirectExpression(expressions_1.CDSName);
|
|
73
|
+
const alias = asName === undefined ? bareAlias : asName;
|
|
74
|
+
const lookup = ddic.lookupTableOrView(entity);
|
|
75
|
+
if (lookup.type instanceof basic_1.UnknownType && lookup.object === undefined && this.reg.inErrorNamespace(entity)) {
|
|
76
|
+
issues.push(issue_1.Issue.atToken(file, entityNode.getFirstToken(), `CDS source "${entity}" not found`, this.getMetadata().key, this.conf.severity));
|
|
77
|
+
}
|
|
78
|
+
if (lookup.type instanceof basic_1.StructureType || lookup.type instanceof basic_1.UnknownType || lookup.type instanceof basic_1.VoidType) {
|
|
79
|
+
const names = [entity.toUpperCase()];
|
|
80
|
+
if (alias) {
|
|
81
|
+
names.push(this.name(alias).toUpperCase());
|
|
82
|
+
}
|
|
83
|
+
sources.push({ entity, names, type: lookup.type });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return sources;
|
|
87
|
+
}
|
|
88
|
+
checkRelations(tree, ddic, issues, file) {
|
|
89
|
+
for (const relation of tree.findAllExpressionsRecursive(expressions_1.CDSRelation)) {
|
|
90
|
+
const directTokens = relation.getDirectTokens();
|
|
91
|
+
if (directTokens.length === 0) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const entity = directTokens.map(t => t.getStr()).join("");
|
|
95
|
+
const lookup = ddic.lookupTableOrView(entity);
|
|
96
|
+
if (lookup.type instanceof basic_1.UnknownType && lookup.object === undefined && this.reg.inErrorNamespace(entity)) {
|
|
97
|
+
issues.push(issue_1.Issue.atToken(file, directTokens[0], `CDS source "${entity}" not found`, this.getMetadata().key, this.conf.severity));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
checkTypes(tree, ddic, issues, file) {
|
|
102
|
+
for (const typeNode of tree.findAllExpressionsRecursive(expressions_1.CDSType)) {
|
|
103
|
+
const typeName = typeNode.findDirectExpressions(expressions_1.CDSName).map(n => this.name(n)).join(".");
|
|
104
|
+
if (typeName === "" || typeName.toUpperCase().startsWith("ABAP.")) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const lookup = ddic.lookup(typeName);
|
|
108
|
+
if (lookup.type instanceof basic_1.UnknownType && lookup.object === undefined && this.reg.inErrorNamespace(typeName)) {
|
|
109
|
+
issues.push(issue_1.Issue.atToken(file, typeNode.getFirstToken(), `CDS type "${typeName}" not found`, this.getMetadata().key, this.conf.severity));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
checkRootProjection(tree, ddic, issues, file) {
|
|
114
|
+
var _a;
|
|
115
|
+
const projection = tree.findFirstExpression(expressions_1.CDSDefineProjection);
|
|
116
|
+
const rootToken = projection === null || projection === void 0 ? void 0 : projection.findDirectTokenByText("ROOT");
|
|
117
|
+
if (projection === undefined || rootToken === undefined) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const names = projection.findDirectExpressions(expressions_1.CDSName);
|
|
121
|
+
const projectedEntityNode = names[1];
|
|
122
|
+
if (projectedEntityNode === undefined) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const projectedEntityName = this.name(projectedEntityNode);
|
|
126
|
+
const projectedEntity = ddic.lookupTableOrView(projectedEntityName).object;
|
|
127
|
+
if (projectedEntity instanceof objects_1.DataDefinition
|
|
128
|
+
&& ((_a = projectedEntity.getTree()) === null || _a === void 0 ? void 0 : _a.findDirectTokenByText("ROOT")) === undefined) {
|
|
129
|
+
const message = `ROOT keyword not valid since ${projectedEntityName} is not a root property`;
|
|
130
|
+
issues.push(issue_1.Issue.atToken(file, rootToken, message, this.getMetadata().key, this.conf.severity));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
checkFieldAnnotations(tree, ddic, issues, file) {
|
|
134
|
+
const definition = tree.findFirstExpression(expressions_1.CDSDefineAbstract);
|
|
135
|
+
if (definition === undefined) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
let definitionNameSeen = false;
|
|
139
|
+
let fieldName;
|
|
140
|
+
let annotations = [];
|
|
141
|
+
const fields = [];
|
|
142
|
+
for (const child of definition.getChildren()) {
|
|
143
|
+
if (child instanceof nodes_1.TokenNode) {
|
|
144
|
+
if (child.get().getStr() === ";") {
|
|
145
|
+
fieldName = undefined;
|
|
146
|
+
annotations = [];
|
|
147
|
+
}
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
else if (child.get() instanceof expressions_1.CDSAnnotation) {
|
|
151
|
+
annotations.push(child);
|
|
152
|
+
}
|
|
153
|
+
else if (child.get() instanceof expressions_1.CDSName) {
|
|
154
|
+
if (definitionNameSeen === false) {
|
|
155
|
+
definitionNameSeen = true;
|
|
156
|
+
annotations = [];
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
fieldName = child;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (child.get() instanceof expressions_1.CDSType && fieldName !== undefined) {
|
|
163
|
+
const semanticType = this.determineSemanticType(child, ddic);
|
|
164
|
+
const normalizedAnnotations = annotations.map(a => a.concatTokens().replace(/\s/g, ""));
|
|
165
|
+
fields.push({ name: this.name(fieldName), node: fieldName, annotations: normalizedAnnotations, semanticType });
|
|
166
|
+
fieldName = undefined;
|
|
167
|
+
annotations = [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const reportedCompanionFields = new Set();
|
|
171
|
+
for (const field of fields) {
|
|
172
|
+
if (field.semanticType === undefined) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const quantity = field.semanticType === "QUAN";
|
|
176
|
+
const referenceAnnotation = quantity ? "@Semantics.quantity.unitOfMeasure" : "@Semantics.amount.currencyCode";
|
|
177
|
+
const companionAnnotation = quantity ? "@Semantics.unitOfMeasure: true" : "@Semantics.currencyCode: true";
|
|
178
|
+
const reference = this.findSemanticReference(field.annotations, field.semanticType);
|
|
179
|
+
if (reference === undefined) {
|
|
180
|
+
const kind = quantity ? "quantity" : "amount";
|
|
181
|
+
const message = `CDS ${kind} field "${field.name}" requires ${referenceAnnotation}`;
|
|
182
|
+
issues.push(issue_1.Issue.atToken(file, field.node.getFirstToken(), message, this.getMetadata().key, this.conf.severity));
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const companion = fields.find(f => f.name.toUpperCase() === reference.toUpperCase());
|
|
186
|
+
if (companion === undefined) {
|
|
187
|
+
const kind = quantity ? "unit" : "currency";
|
|
188
|
+
const message = `CDS field "${field.name}" references missing ${kind} field "${reference}"`;
|
|
189
|
+
issues.push(issue_1.Issue.atToken(file, field.node.getFirstToken(), message, this.getMetadata().key, this.conf.severity));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const companionKey = field.semanticType + ":" + companion.name.toUpperCase();
|
|
193
|
+
if (reportedCompanionFields.has(companionKey) === false
|
|
194
|
+
&& this.hasCompanionAnnotation(companion.annotations, field.semanticType) === false) {
|
|
195
|
+
const message = `CDS field "${companion.name}" requires ${companionAnnotation}`;
|
|
196
|
+
issues.push(issue_1.Issue.atToken(file, companion.node.getFirstToken(), message, this.getMetadata().key, this.conf.severity));
|
|
197
|
+
reportedCompanionFields.add(companionKey);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
determineSemanticType(typeNode, ddic) {
|
|
202
|
+
var _a;
|
|
203
|
+
const typeName = typeNode.findDirectExpressions(expressions_1.CDSName).map(n => this.name(n)).join(".");
|
|
204
|
+
const upper = typeName.toUpperCase();
|
|
205
|
+
if (upper === "ABAP.QUAN" || KNOWN_QUAN_DATA_ELEMENTS.includes(upper)) {
|
|
206
|
+
return "QUAN";
|
|
207
|
+
}
|
|
208
|
+
else if (upper === "ABAP.CURR" || KNOWN_CURR_DATA_ELEMENTS.includes(upper)) {
|
|
209
|
+
return "CURR";
|
|
210
|
+
}
|
|
211
|
+
const object = ddic.lookup(typeName).object;
|
|
212
|
+
if (object instanceof objects_1.DataElement) {
|
|
213
|
+
const dataType = (_a = object.getDataType(this.reg)) === null || _a === void 0 ? void 0 : _a.toUpperCase();
|
|
214
|
+
if (dataType === "QUAN" || dataType === "CURR") {
|
|
215
|
+
return dataType;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
findSemanticReference(annotations, type) {
|
|
221
|
+
const expression = type === "QUAN"
|
|
222
|
+
? /(?:SEMANTICS\.QUANTITY|SEMANTICS:\{QUANTITY:\{)\.?UNITOFMEASURE:'([^']+)'/i
|
|
223
|
+
: /(?:SEMANTICS\.AMOUNT|SEMANTICS:\{AMOUNT:\{)\.?CURRENCYCODE:'([^']+)'/i;
|
|
224
|
+
for (const annotation of annotations) {
|
|
225
|
+
const match = annotation.match(expression);
|
|
226
|
+
if (match) {
|
|
227
|
+
return match[1];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
hasCompanionAnnotation(annotations, type) {
|
|
233
|
+
if (type === "QUAN") {
|
|
234
|
+
return annotations.some(a => a.toUpperCase().includes("@SEMANTICS.UNITOFMEASURE:TRUE")
|
|
235
|
+
|| a.toUpperCase().includes("@SEMANTICS:{UNITOFMEASURE:TRUE"));
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
return annotations.some(a => a.toUpperCase().includes("@SEMANTICS.CURRENCYCODE:TRUE")
|
|
239
|
+
|| a.toUpperCase().includes("@SEMANTICS:{CURRENCYCODE:TRUE"));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
checkFields(tree, sources, object, issues, file) {
|
|
243
|
+
var _a;
|
|
244
|
+
const associationNames = new Set();
|
|
245
|
+
for (const association of ((_a = object.getParsedData()) === null || _a === void 0 ? void 0 : _a.associations) || []) {
|
|
246
|
+
associationNames.add(association.name.toUpperCase());
|
|
247
|
+
if (association.as) {
|
|
248
|
+
associationNames.add(association.as.toUpperCase());
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const element of tree.findAllExpressionsRecursive(expressions_1.CDSElement)) {
|
|
252
|
+
const prefixed = element.findDirectExpression(expressions_1.CDSPrefixedName);
|
|
253
|
+
if (prefixed === undefined) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const names = prefixed.findDirectExpressions(expressions_1.CDSName);
|
|
257
|
+
if (names.length === 0) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const first = this.name(names[0]);
|
|
261
|
+
if (first === "*" || first.startsWith("$") || first.startsWith("#") || associationNames.has(first.toUpperCase())) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (names.length > 1) {
|
|
265
|
+
const matchingSources = sources.filter(s => s.names.includes(first.toUpperCase()));
|
|
266
|
+
if (matchingSources.length !== 1) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const source = matchingSources[0];
|
|
270
|
+
const sourceType = source.type;
|
|
271
|
+
if (!(sourceType instanceof basic_1.StructureType)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const field = this.name(names[1]);
|
|
275
|
+
if (field !== "*" && sourceType.getComponentByName(field) === undefined) {
|
|
276
|
+
issues.push(issue_1.Issue.atToken(file, names[1].getFirstToken(), `CDS field "${field}" not found in "${source.entity}"`, this.getMetadata().key, this.conf.severity));
|
|
277
|
+
}
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
// Do not guess across joins or when a dependency is absent.
|
|
281
|
+
if (sources.length !== 1) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const source = sources[0];
|
|
285
|
+
const sourceType = source.type;
|
|
286
|
+
if (!(sourceType instanceof basic_1.StructureType)) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (sourceType.getComponentByName(first) === undefined) {
|
|
290
|
+
issues.push(issue_1.Issue.atToken(file, names[0].getFirstToken(), `CDS field "${first}" not found in "${source.entity}"`, this.getMetadata().key, this.conf.severity));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
name(node) {
|
|
295
|
+
return node.concatTokens().replace(/ /g, "");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
exports.CDSCheckSyntax = CDSCheckSyntax;
|
|
299
|
+
//# sourceMappingURL=cds_check_syntax.js.map
|
package/build/src/rules/index.js
CHANGED
|
@@ -30,6 +30,7 @@ __exportStar(require("./begin_single_include"), exports);
|
|
|
30
30
|
__exportStar(require("./call_transaction_authority_check"), exports);
|
|
31
31
|
__exportStar(require("./catch_and_raise"), exports);
|
|
32
32
|
__exportStar(require("./cds_association_name"), exports);
|
|
33
|
+
__exportStar(require("./cds_check_syntax"), exports);
|
|
33
34
|
__exportStar(require("./cds_comment_style"), exports);
|
|
34
35
|
__exportStar(require("./cds_field_order"), exports);
|
|
35
36
|
__exportStar(require("./cds_legacy_view"), exports);
|