@sap/eslint-plugin-cds 2.0.5 → 2.2.2

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.
@@ -0,0 +1,171 @@
1
+ module.exports = require("../../api").createRule({
2
+ meta: {
3
+ docs: {
4
+ description: `Ambiguous key with a \`TO MANY\` relationship since entries could appear multiple times with the same key.`,
5
+ category: "Model Validation",
6
+ version: "1.0.1",
7
+ },
8
+ type: "problem",
9
+ },
10
+ create(context) {
11
+ let csnOdata;
12
+ const m = context.cds.model;
13
+ if (m && m.definitions) {
14
+ try {
15
+ csnOdata = context.cds.compile.for.odata(m);
16
+ const csnOdataLinked = context.cds.linked(csnOdata);
17
+ associationCardinalityFlaw(csnOdataLinked, context);
18
+ } catch (err) {
19
+ // Don't continue with rule if model fails to compile
20
+ }
21
+ }
22
+ },
23
+ });
24
+
25
+ function associationCardinalityFlaw(csn, context) {
26
+ processEntity(csn, (definition, sourceEntity, sourceAlias) => {
27
+ let refCardinalityMult = false;
28
+ let refPlainElement = false;
29
+ processElement(
30
+ csn,
31
+ definition,
32
+ sourceEntity,
33
+ sourceAlias,
34
+ () => {
35
+ refCardinalityMult = false;
36
+ refPlainElement = false;
37
+ },
38
+ (refEntity, refElement) => {
39
+ if (
40
+ refElement.type === "cds.Association" ||
41
+ refElement.type === "cds.Composition"
42
+ ) {
43
+ if (refElement.cardinality && refElement.cardinality.max === "*") {
44
+ refCardinalityMult = true;
45
+ }
46
+ } else {
47
+ refPlainElement = true;
48
+ }
49
+ },
50
+ (column) => {
51
+ if (
52
+ definition.keys &&
53
+ Object.keys(definition.keys).length === 1 &&
54
+ Object.keys(definition.keys)[0] === "ID" &&
55
+ refCardinalityMult &&
56
+ refPlainElement
57
+ ) {
58
+ const loc = context.cds.getLocation(definition.name, definition);
59
+ context.report({
60
+ message: `Ambiguous key in '${definition.name}'. Element '${
61
+ column.as ? column.as : column.name
62
+ }' leads to multiple entries so that key '${
63
+ Object.keys(definition.keys)[0]
64
+ }' is not unique.`,
65
+ loc,
66
+ file: definition.$location.file,
67
+ });
68
+ }
69
+ }
70
+ );
71
+ });
72
+ }
73
+
74
+ function processEntity(csn, eachCallback) {
75
+ Object.keys(csn.definitions).forEach((name) => {
76
+ if (name.startsWith("localized.")) {
77
+ return;
78
+ }
79
+ const definition = csn.definitions[name];
80
+ if (
81
+ definition.kind === "entity" &&
82
+ definition.query &&
83
+ definition.query.SELECT &&
84
+ definition.query.SELECT.columns
85
+ ) {
86
+ let sourceEntity;
87
+ const sourceAlias = [];
88
+ if (definition.query.SELECT.from.ref) {
89
+ // From
90
+ sourceEntity =
91
+ csn.definitions[definition.query.SELECT.from.ref.join("_")];
92
+ sourceAlias.push({
93
+ from: sourceEntity.name,
94
+ as:
95
+ definition.query.SELECT.from.as ||
96
+ definition.query.SELECT.from.ref.slice(-1)[0].split(".").pop(),
97
+ });
98
+ } else if (
99
+ definition.query.SELECT.from.args &&
100
+ definition.query.SELECT.from.args[0].ref
101
+ ) {
102
+ // Join
103
+ sourceEntity =
104
+ csn.definitions[definition.query.SELECT.from.args[0].ref.join("_")];
105
+ definition.query.SELECT.from.args.forEach((arg) => {
106
+ sourceAlias.push({
107
+ from: arg.ref.join("_"),
108
+ as: arg.as || arg.ref.slice(-1)[0].split(".").pop(),
109
+ });
110
+ });
111
+ }
112
+ if (!sourceEntity) {
113
+ return;
114
+ }
115
+ eachCallback(definition, sourceEntity, sourceAlias);
116
+ }
117
+ });
118
+ }
119
+
120
+ function processElement(
121
+ csn,
122
+ definition,
123
+ sourceEntity,
124
+ sourceAlias,
125
+ beforeCallback,
126
+ eachCallback,
127
+ afterCallback
128
+ ) {
129
+ definition.query.SELECT.columns.forEach((column) => {
130
+ if (column.ref && column.ref.length > 1) {
131
+ let refEntity = sourceEntity;
132
+ let refAlias = sourceAlias;
133
+ beforeCallback();
134
+ column.ref.forEach((ref) => {
135
+ ref = ref.id || ref;
136
+ // Alias
137
+ const matchAlias = refAlias.find((alias) => {
138
+ return alias.as === ref;
139
+ });
140
+ let refElement;
141
+ if (matchAlias) {
142
+ refEntity = csn.definitions[matchAlias.from];
143
+ } else {
144
+ refElement = refEntity.elements[ref];
145
+ // Mixin
146
+ if (!refElement) {
147
+ refElement = definition.elements[ref];
148
+ if (!refElement && definition.query.SELECT.mixin) {
149
+ refElement = definition.query.SELECT.mixin[ref];
150
+ if (!refElement && definition.query.SELECT.mixin[column.ref[0]]) {
151
+ refElement =
152
+ definition.query.SELECT.mixin[column.ref[0]]._target.elements[
153
+ ref
154
+ ];
155
+ }
156
+ }
157
+ }
158
+ eachCallback(refEntity, refElement);
159
+ if (
160
+ refElement.type === "cds.Association" ||
161
+ refElement.type === "cds.Composition"
162
+ ) {
163
+ refEntity = csn.definitions[refElement.target];
164
+ }
165
+ }
166
+ refAlias = [];
167
+ });
168
+ afterCallback(column);
169
+ }
170
+ });
171
+ }
@@ -0,0 +1,35 @@
1
+ module.exports = require("../../api").createRule({
2
+ meta: {
3
+ docs: {
4
+ description: `Checks whether the CDS model file can be compiled by the @sap/cds wihtout errors.`,
5
+ category: "Model Validation",
6
+ version: "1.0.0",
7
+ },
8
+ type: "problem",
9
+ },
10
+ create: function (context) {
11
+ const m = context.cds.model;
12
+ if (m.err) {
13
+ if (m.err) {
14
+ // If any csn compile errors occur
15
+ m.err.messages.forEach((err) => {
16
+ const msg = err.message;
17
+ let file = "";
18
+ const loc = {
19
+ start: { line: 0, column: 0 },
20
+ end: { line: 1, column: 0 },
21
+ };
22
+ // Get its location if it exists
23
+ if (err.$location) {
24
+ loc.start.column = err.$location.col;
25
+ loc.start.line = err.$location.line;
26
+ loc.end.column = err.$location.endCol;
27
+ loc.end.line = err.$location.endLine;
28
+ file = err.$location.file;
29
+ }
30
+ context.report({ message: `${msg}`, loc, file });
31
+ });
32
+ }
33
+ }
34
+ },
35
+ });
@@ -1,40 +1,46 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- const ruleFactory_1 = require("../ruleFactory");
6
- const child_process_1 = __importDefault(require("child_process"));
7
- const semver_1 = __importDefault(require("semver"));
8
- module.exports = ruleFactory_1.createRule({
9
- type: 'suggestion',
1
+ const cp = require("child_process");
2
+ const semver = require("semver");
3
+
4
+ module.exports = require("../../api").createRule({
5
+ meta: {
10
6
  docs: {
11
- description: `Checks whether the latest cds version is being used.`,
12
- category: 'Environment',
13
- version: '1.0.4'
7
+ description: "Checks whether the latest cds version is being used.",
8
+ category: "Environment",
9
+ version: "1.0.4",
14
10
  },
15
- }, (cds, context) => {
16
- const report = [];
11
+ type: "suggestion",
12
+ hasSuggestions: true,
13
+ messages: {
14
+ latestCDSVersion: `A newer CDS version is available!`,
15
+ },
16
+ },
17
+ create: function (context) {
17
18
  let result;
18
19
  let cdsVersions;
19
- if (!cds.environment) {
20
- try {
21
- result = child_process_1.default.execSync(`npm outdated @sap/cds --json`, {
22
- cwd: process.cwd()
23
- }).toString();
24
- cdsVersions = JSON.parse(result)['@sap/cds'];
25
- }
26
- catch (err) {
27
- }
28
- }
29
- else {
30
- cdsVersions = cds.environment['@sap/cds'];
20
+ const e = context.cds.environment;
21
+ if (!e) {
22
+ try {
23
+ result = cp
24
+ .execSync(`npm outdated @sap/cds --json`, {
25
+ cwd: process.cwd(),
26
+ })
27
+ .toString();
28
+ cdsVersions = JSON.parse(result)["@sap/cds"];
29
+ } catch (err) {
30
+ // Do not throw
31
+ }
32
+ } else {
33
+ cdsVersions = context.cds.environment["@sap/cds"];
31
34
  }
32
- if (Object.keys(cdsVersions).length !== 0 && !semver_1.default.satisfies(cdsVersions.latest, cdsVersions.current)) {
33
- report.push({
34
- message: `A newer CDS version ${cdsVersions.latest} is available!`,
35
- loc: { line: 0, column: 0 }
36
- });
35
+ // If current cds version is not the latest
36
+ if (
37
+ Object.keys(cdsVersions).length !== 0 &&
38
+ !semver.satisfies(cdsVersions.latest, cdsVersions.current)
39
+ ) {
40
+ // Add to ESLint report
41
+ context.report({
42
+ messageId: "latestCDSVersion",
43
+ });
37
44
  }
38
- return report;
45
+ },
39
46
  });
40
- //# sourceMappingURL=latest-cds-version.js.map
@@ -1,42 +1,42 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- const ruleFactory_1 = require("../ruleFactory");
6
- const semver_1 = __importDefault(require("semver"));
7
- const path_1 = __importDefault(require("path"));
8
- module.exports = ruleFactory_1.createRule({
9
- type: 'problem',
1
+ const path = require("path");
2
+ const semver = require("semver");
3
+
4
+ module.exports = require("../../api").createRule({
5
+ meta: {
10
6
  docs: {
11
- description: `Checks whether the minimum node version required by the <code>@sap/cds</code> is achieved.`,
12
- category: 'Environment',
13
- version: '1.0.0'
7
+ description: `Checks whether the minimum node version required by the \`@sap/cds\` is achieved.`,
8
+ category: "Environment",
9
+ version: "1.0.0",
14
10
  },
15
- }, (cds, context) => {
16
- var _a;
17
- const report = [];
18
- const filepath = context.getFilename();
11
+ type: "problem",
12
+ },
13
+ create: function (context) {
14
+ const e = context.cds.environment;
19
15
  let nodeVersion, nodeVersionCDS;
20
- if (!cds.environment) {
21
- try {
22
- const CDSPath = require.resolve("@sap/cds/package.json", { paths: [path_1.default.dirname(filepath)] });
23
- const jsonCDS = require(CDSPath);
24
- nodeVersion = process.version;
25
- nodeVersionCDS = (_a = jsonCDS.engines) === null || _a === void 0 ? void 0 : _a.node;
26
- }
27
- catch (_b) {
28
- }
29
- }
30
- else {
31
- nodeVersion = cds.environment.nodeVersion;
32
- nodeVersionCDS = cds.environment.nodeVersionCDS;
33
- }
34
- if (nodeVersion && nodeVersionCDS && !semver_1.default.satisfies(nodeVersion, nodeVersionCDS, { loose: true })) {
35
- report.push({
36
- message: `CDS minimum node version of ${nodeVersionCDS} required, found ${nodeVersion}!`,
37
- loc: { line: 0, column: 1 }
16
+ if (!e) {
17
+ // Get current and required node versions
18
+ try {
19
+ const CDSPath = require.resolve("@sap/cds/package.json", {
20
+ paths: [path.dirname(context.filePath)],
38
21
  });
22
+ const jsonCDS = require(CDSPath);
23
+ nodeVersion = process.version;
24
+ nodeVersionCDS = jsonCDS.engines.node;
25
+ } catch (err) {
26
+ // Do not throw
27
+ }
28
+ } else {
29
+ nodeVersion = context.cds.environment.nodeVersion;
30
+ nodeVersionCDS = context.cds.environment.nodeVersionCDS;
31
+ }
32
+ if (
33
+ nodeVersion &&
34
+ nodeVersionCDS &&
35
+ !semver.satisfies(nodeVersion, nodeVersionCDS, { loose: true })
36
+ ) {
37
+ context.report({
38
+ message: `CDS minimum node version of ${nodeVersionCDS} required, found ${nodeVersion}!`,
39
+ });
39
40
  }
40
- return report;
41
+ },
41
42
  });
42
- //# sourceMappingURL=min-node-version.js.map
@@ -0,0 +1,35 @@
1
+ module.exports = require("../../api").defineRule({
2
+ meta: {
3
+ docs: {
4
+ description: `Avoid using reserved SQL keywords.`,
5
+ category: "Model Validation",
6
+ },
7
+ },
8
+ create(context) {
9
+ const { db = { kind: "sql" } } = context.cds.env.requires;
10
+ function _check(d) {
11
+ if (d.name in RESERVED) {
12
+ // Do not blame in case of external services
13
+ let srv = d._service || (d.parent && d.parent._service);
14
+ if (srv && srv["@cds.external"]) return;
15
+
16
+ // Do blame
17
+ return `'${d.name}' is a reserved keyword in ${db.kind.toUpperCase()}`;
18
+ }
19
+ }
20
+ return { entity: _check, element: _check };
21
+ },
22
+ });
23
+
24
+ // REVISIT: Replace by compiler-provided check
25
+ const RESERVED = {
26
+ ORDER: 1,
27
+ Order: 1,
28
+ order: 1,
29
+ GROUP: 1,
30
+ Group: 1,
31
+ group: 1,
32
+ LIMIT: 1,
33
+ Limit: 1,
34
+ limit: 1,
35
+ };
@@ -0,0 +1,35 @@
1
+ module.exports = require("../../api").createRule({
2
+ meta: {
3
+ docs: {
4
+ description: `Draft-enabled entities shall not be used in views that make use of \`JOIN\`.`,
5
+ category: "Model Validation",
6
+ version: "2.2.1",
7
+ },
8
+ type: "suggestion",
9
+ messages: {
10
+ noJoinOnDraftEnabledEntities: `Do not use draft-enabled entities in views that make use of \`JOIN\`.`,
11
+ },
12
+ },
13
+ create: function (context) {
14
+ const m = context.cds.model;
15
+ m.foreach("entity", (entity) => {
16
+ if (entity["@odata.draft.enabled"]) {
17
+ if (entity.query.SELECT.from.join) {
18
+ const location = entity.query.$location;
19
+ if (context.sourcecode.lines[location.line - 1]) {
20
+ const endCol = context.sourcecode.lines[location.line - 1].length;
21
+ const loc = {
22
+ start: { line: location.line, column: location.col - 1 },
23
+ end: { line: location.line, column: endCol },
24
+ };
25
+ context.report({
26
+ messageId: "noJoinOnDraftEnabledEntities",
27
+ loc,
28
+ file: entity.$location.file,
29
+ });
30
+ }
31
+ }
32
+ }
33
+ });
34
+ },
35
+ });
@@ -0,0 +1,29 @@
1
+ module.exports = require("../../api").createRule({
2
+ meta: {
3
+ docs: {
4
+ description: `Foreign key information of a \`TO MANY\` relationship must be defined within the target and specified in an \`ON\` condition.`,
5
+ category: "Model Validation",
6
+ version: "2.1.0",
7
+ },
8
+ type: "problem",
9
+ },
10
+ create: function (context) {
11
+ const m = context.cds.model;
12
+ m.forall((d) => {
13
+ if (d.name) {
14
+ if (!d.elements) return;
15
+ for (const elementName in d.elements) {
16
+ const element = d.elements[elementName];
17
+ if (element.is2many && !element.on) {
18
+ const loc = context.cds.getLocation(elementName, element);
19
+ context.report({
20
+ message: `You must provide an \`ON\` condition for \`TO MANY\` relationship '${element.name}'.`,
21
+ loc,
22
+ file: d.$location.file,
23
+ });
24
+ }
25
+ }
26
+ }
27
+ });
28
+ },
29
+ });
@@ -0,0 +1,20 @@
1
+ // @ts-check
2
+ module.exports = require("../../api").createRule({
3
+ meta: {
4
+ docs: {
5
+ description: "{{description}}",
6
+ version: "{{version}}"
7
+ },
8
+ type:"{{type}}",
9
+ },
10
+ create: function(context) {
11
+ const m = cotext.cds.model;
12
+ m.forall((d)) => {
13
+ // Add cds logic here, for example
14
+ return [{
15
+ message: "{{messages}}",
16
+ loc: {{loc}}
17
+ }];
18
+ }
19
+ }
20
+ })
@@ -1,44 +1,50 @@
1
- "use strict";
2
- const ruleFactory_1 = require("../ruleFactory");
3
- function suggestSQLCast(m) {
4
- const results = [];
5
- const view = (d) => d.query;
6
- const check = (m) => m.foreach(view, (v) => {
1
+ module.exports = require("../../api").createRule({
2
+ meta: {
3
+ docs: {
4
+ description: "Should make suggestions for possible missing sql casts.",
5
+ category: "Model Validation",
6
+ version: "1.0.8",
7
+ },
8
+ type: "suggestion",
9
+ hasSuggestions: true,
10
+ messages: {
11
+ missingSQLCast:
12
+ "Potential issue - Missing SQL cast for column expression?",
13
+ },
14
+ },
15
+ create: function (context) {
16
+ const m = context.cds.model;
17
+ if (m) {
18
+ const view = (d) => d.query;
19
+ m.foreach(view, (v) => {
7
20
  if (v.query.SET)
8
- for (const { SELECT } of v.query.SET.args) {
9
- for (const each of SELECT.columns || []) {
10
- const { xpr, cast, $location: loc } = each;
11
- if (cast && xpr) {
12
- if (xpr[0].xpr && xpr[0].xpr && xpr[0].cast) {
13
- continue;
14
- }
15
- else {
16
- results.push({ message: `Potential issue: Missing SQL cast for column expression?`, loc, file: loc.file });
17
- }
18
- }
21
+ for (const { SELECT } of v.query.SET.args) {
22
+ // Only in UNION cases?
23
+ for (const each of SELECT.columns || []) {
24
+ const { xpr, cast, $location: location } = each;
25
+ if (cast && xpr) {
26
+ if (xpr[0].xpr && xpr[0].xpr && xpr[0].cast) {
27
+ continue;
28
+ } else {
29
+ if (context.sourcecode.lines[location.line - 1]) {
30
+ const endCol =
31
+ context.sourcecode.lines[location.line - 1].length;
32
+ const loc = {
33
+ start: { line: location.line, column: location.col - 1 },
34
+ end: { line: location.line, column: endCol },
35
+ };
36
+ context.report({
37
+ messageId: "missingSQLCast",
38
+ loc,
39
+ file: location.file,
40
+ });
41
+ }
19
42
  }
43
+ }
20
44
  }
21
- });
22
- check(m);
23
- return results;
24
- }
25
- module.exports = ruleFactory_1.createRule({
26
- type: 'problem',
27
- docs: {
28
- description: `Should make suggestions for possible missing sql casts.`,
29
- category: 'Model Validation',
30
- version: '1.0.8'
31
- },
32
- fixable: 'code'
33
- }, (cds, context) => {
34
- let report = [];
35
- const filepath = context.getFilename();
36
- if (filepath.endsWith('.cds')) {
37
- const m = cds.linked(cds.model);
38
- if (m) {
39
- report = suggestSQLCast(m);
40
- }
45
+ }
46
+ });
41
47
  }
42
- return report;
48
+ return context.report;
49
+ },
43
50
  });
44
- //# sourceMappingURL=sql-cast-suggestion.js.map
@@ -0,0 +1,74 @@
1
+ module.exports = require("../../api").createRule({
2
+ meta: {
3
+ docs: {
4
+ description: "Regular element names should start with lowercase letters.",
5
+ category: "Model Validation",
6
+ version: "1.0.4",
7
+ },
8
+ type: "suggestion",
9
+ hasSuggestions: true,
10
+ messages: {
11
+ startLowercase:
12
+ "Element name '{{entityName}}.{{elementName}}' should start with a lowercase letter.",
13
+ },
14
+ fixable: "code",
15
+ },
16
+ create: function (context) {
17
+ const m = context.cds.model;
18
+ if (m && m.definitions) {
19
+ m.forall((d) => {
20
+ const entityName = d.name;
21
+ for (const elementName in d.elements) {
22
+ const element = d.elements[elementName];
23
+ if (
24
+ elementName &&
25
+ !(
26
+ entityName.startsWith("localized") || entityName.endsWith("texts")
27
+ )
28
+ ) {
29
+ if (
30
+ elementName.charAt(0) !== elementName.charAt(0).toLowerCase() &&
31
+ !["ID"].includes(elementName)
32
+ ) {
33
+ if (element.$location && element.$location.file) {
34
+ const file = element.$location.file;
35
+ const loc = context.cds.getLocation(elementName, element);
36
+ const fix = (fixer) => {
37
+ const elementNameSanitized =
38
+ elementName.charAt(0).toLowerCase() + elementName.slice(1);
39
+ const rangeEnd = context.sourcecode.getIndexFromLoc({
40
+ line: loc.end.line,
41
+ column: loc.end.column,
42
+ });
43
+ const rangeBeg = rangeEnd
44
+ ? rangeEnd - elementNameSanitized.length
45
+ : 0;
46
+ return fixer.replaceTextRange(
47
+ [rangeBeg, rangeEnd],
48
+ elementNameSanitized
49
+ );
50
+ };
51
+ context.report({
52
+ messageId: "startLowercase",
53
+ loc,
54
+ file,
55
+ fix,
56
+ data: {
57
+ entityName,
58
+ elementName,
59
+ },
60
+ suggest: [
61
+ {
62
+ messageId: "startLowercase",
63
+ fix,
64
+ },
65
+ ],
66
+ });
67
+ }
68
+ }
69
+ }
70
+ }
71
+ });
72
+ }
73
+ },
74
+ });