@sap/eslint-plugin-cds 2.1.1 → 2.3.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.
@@ -1,151 +1,173 @@
1
- const cdsLint = require("../../api");
2
-
3
- module.exports = cdsLint.createRule(
4
- /* Rule meta */
5
- {
1
+ module.exports = require("../../api").createRule({
2
+ meta: {
6
3
  docs: {
7
- description: "Ambiguous key with a to-many relationship. Entries could appear multiple times with the same key if they point to an entity using a to-many relationship.",
8
- category: "Model Validation",
9
- version: "1.0.1",
4
+ description: "Ambiguous key with a `TO MANY` relationship since entries could appear multiple times with the same key.",
5
+ category: "Model Validation",
6
+ recommended: true,
7
+ version: "1.0.1",
10
8
  },
11
- type: "problem",
9
+ severity: "warn",
10
+ type: "problem"
12
11
  },
13
- /* Rule logic */
14
- (report, cds) => {
15
- return createRuleReport(report, cds) || report;
16
- }
17
- );
18
-
19
- const createRuleReport = (report, cds) => {
12
+ create(context) {
20
13
  let csnOdata;
21
- const m = cds.model;
14
+ const m = context.cds.model;
22
15
  if (m && m.definitions) {
23
- csnOdata = cds.compile.for.odata(m);
24
- const csnOdataLinked = cds.linked(csnOdata);
25
- associationCardinalityFlaw(csnOdataLinked, report, cds);
16
+ try {
17
+ csnOdata = context.cds.compile.for.odata(m);
18
+ const csnOdataLinked = context.cds.linked(csnOdata);
19
+ associationCardinalityFlaw(csnOdataLinked, context);
20
+ } catch (err) {
21
+ // Don't continue with rule if model fails to compile
22
+ }
26
23
  }
27
- return report;
28
- };
29
-
30
-
31
- function associationCardinalityFlaw(csn, report, cds) {
32
- const messages = [];
33
- processEntity(csn, (definition, sourceEntity, sourceAlias) => {
34
- let refCardinalityMult = false;
35
- let refPlainElement = false;
36
- processElement(
37
- csn,
38
- definition,
39
- sourceEntity,
40
- sourceAlias,
41
- () => {
42
- refCardinalityMult = false;
43
- refPlainElement = false;
44
- },
45
- (refEntity, refElement) => {
46
- if (refElement.type === "cds.Association" || refElement.type === "cds.Composition") {
47
- if (refElement.cardinality && refElement.cardinality.max === "*") {
48
- refCardinalityMult = true;
49
- }
50
- } else {
51
- refPlainElement = true;
52
- }
53
- },
54
- (column) => {
55
- if (
56
- definition.keys &&
57
- Object.keys(definition.keys).length === 1 &&
58
- Object.keys(definition.keys)[0] === "ID" &&
59
- refCardinalityMult &&
60
- refPlainElement
61
- ) {
62
- const loc = cds.getLocation(definition.name, definition);
63
- report.push({
64
- message: `Ambiguous key in '${definition.name}'. Element '${column.as ? column.as : column.name}' leads to multiple entries so that key '${Object.keys(definition.keys)[0]}' is not unique.`,
65
- loc,
66
- file: definition.$location.file,
67
- });
68
- }
69
- }
70
- );
71
- });
72
- return [messages];
73
- }
24
+ },
25
+ });
74
26
 
75
- function processEntity(csn, eachCallback) {
76
- Object.keys(csn.definitions).forEach((name) => {
77
- if (name.startsWith("localized.")) {
78
- return;
27
+ function associationCardinalityFlaw(csn, context) {
28
+ processEntity(csn, (definition, sourceEntity, sourceAlias) => {
29
+ let refCardinalityMult = false;
30
+ let refPlainElement = false;
31
+ processElement(
32
+ csn,
33
+ definition,
34
+ sourceEntity,
35
+ sourceAlias,
36
+ () => {
37
+ refCardinalityMult = false;
38
+ refPlainElement = false;
39
+ },
40
+ (refEntity, refElement) => {
41
+ if (
42
+ refElement.type === "cds.Association" ||
43
+ refElement.type === "cds.Composition"
44
+ ) {
45
+ if (refElement.cardinality && refElement.cardinality.max === "*") {
46
+ refCardinalityMult = true;
47
+ }
48
+ } else {
49
+ refPlainElement = true;
79
50
  }
80
- const definition = csn.definitions[name];
51
+ },
52
+ (column) => {
81
53
  if (
82
- definition.kind === "entity" &&
83
- definition.query &&
84
- definition.query.SELECT &&
85
- definition.query.SELECT.columns
54
+ definition.keys &&
55
+ Object.keys(definition.keys).length === 1 &&
56
+ Object.keys(definition.keys)[0] === "ID" &&
57
+ refCardinalityMult &&
58
+ refPlainElement
86
59
  ) {
87
- let sourceEntity;
88
- const sourceAlias = [];
89
- if (definition.query.SELECT.from.ref) {
90
- // From
91
- sourceEntity = csn.definitions[definition.query.SELECT.from.ref.join("_")];
92
- sourceAlias.push({
93
- from: sourceEntity.name,
94
- as: definition.query.SELECT.from.as || definition.query.SELECT.from.ref.slice(-1)[0].split(".").pop(),
95
- });
96
- } else if (definition.query.SELECT.from.args && definition.query.SELECT.from.args[0].ref) {
97
- // Join
98
- sourceEntity = csn.definitions[definition.query.SELECT.from.args[0].ref.join("_")];
99
- definition.query.SELECT.from.args.forEach((arg) => {
100
- sourceAlias.push({
101
- from: arg.ref.join("_"),
102
- as: arg.as || arg.ref.slice(-1)[0].split(".").pop(),
103
- });
104
- });
105
- }
106
- if (!sourceEntity) {
107
- return;
108
- }
109
- eachCallback(definition, sourceEntity, sourceAlias);
60
+ const loc = context.cds.getLocation(definition.name, definition);
61
+ context.report({
62
+ message: `Ambiguous key in '${definition.name}'. Element '${
63
+ column.as ? column.as : column.name
64
+ }' leads to multiple entries so that key '${
65
+ Object.keys(definition.keys)[0]
66
+ }' is not unique.`,
67
+ loc,
68
+ file: definition.$location.file,
69
+ });
110
70
  }
111
- });
71
+ }
72
+ );
73
+ });
112
74
  }
113
75
 
114
- function processElement(csn, definition, sourceEntity, sourceAlias, beforeCallback, eachCallback, afterCallback) {
115
- definition.query.SELECT.columns.forEach((column) => {
116
- if (column.ref && column.ref.length > 1) {
117
- let refEntity = sourceEntity;
118
- let refAlias = sourceAlias;
119
- beforeCallback();
120
- column.ref.forEach((ref) => {
121
- ref = ref.id || ref;
122
- // Alias
123
- const matchAlias = refAlias.find((alias) => {
124
- return alias.as === ref;
125
- });
126
- let refElement;
127
- if (matchAlias) {
128
- refEntity = csn.definitions[matchAlias.from];
129
- } else {
130
- refElement = refEntity.elements[ref];
131
- // Mixin
132
- if (!refElement) {
133
- refElement = definition.elements[ref];
134
- if (!refElement && definition.query.SELECT.mixin) {
135
- refElement = definition.query.SELECT.mixin[ref];
136
- if (!refElement && definition.query.SELECT.mixin[column.ref[0]]) {
137
- refElement = definition.query.SELECT.mixin[column.ref[0]]._target.elements[ref];
138
- }
139
- }
140
- }
141
- eachCallback(refEntity, refElement);
142
- if (refElement.type === "cds.Association" || refElement.type === "cds.Composition") {
143
- refEntity = csn.definitions[refElement.target];
144
- }
145
- }
146
- refAlias = [];
147
- });
148
- afterCallback(column);
76
+ function processEntity(csn, eachCallback) {
77
+ Object.keys(csn.definitions).forEach((name) => {
78
+ if (name.startsWith("localized.")) {
79
+ return;
80
+ }
81
+ const definition = csn.definitions[name];
82
+ if (
83
+ definition.kind === "entity" &&
84
+ definition.query &&
85
+ definition.query.SELECT &&
86
+ definition.query.SELECT.columns
87
+ ) {
88
+ let sourceEntity;
89
+ const sourceAlias = [];
90
+ if (definition.query.SELECT.from.ref) {
91
+ // From
92
+ sourceEntity =
93
+ csn.definitions[definition.query.SELECT.from.ref.join("_")];
94
+ sourceAlias.push({
95
+ from: sourceEntity.name,
96
+ as:
97
+ definition.query.SELECT.from.as ||
98
+ definition.query.SELECT.from.ref.slice(-1)[0].split(".").pop(),
99
+ });
100
+ } else if (
101
+ definition.query.SELECT.from.args &&
102
+ definition.query.SELECT.from.args[0].ref
103
+ ) {
104
+ // Join
105
+ sourceEntity =
106
+ csn.definitions[definition.query.SELECT.from.args[0].ref.join("_")];
107
+ definition.query.SELECT.from.args.forEach((arg) => {
108
+ sourceAlias.push({
109
+ from: arg.ref.join("_"),
110
+ as: arg.as || arg.ref.slice(-1)[0].split(".").pop(),
111
+ });
112
+ });
113
+ }
114
+ if (!sourceEntity) {
115
+ return;
116
+ }
117
+ eachCallback(definition, sourceEntity, sourceAlias);
118
+ }
119
+ });
120
+ }
121
+
122
+ function processElement(
123
+ csn,
124
+ definition,
125
+ sourceEntity,
126
+ sourceAlias,
127
+ beforeCallback,
128
+ eachCallback,
129
+ afterCallback
130
+ ) {
131
+ definition.query.SELECT.columns.forEach((column) => {
132
+ if (column.ref && column.ref.length > 1) {
133
+ let refEntity = sourceEntity;
134
+ let refAlias = sourceAlias;
135
+ beforeCallback();
136
+ column.ref.forEach((ref) => {
137
+ ref = ref.id || ref;
138
+ // Alias
139
+ const matchAlias = refAlias.find((alias) => {
140
+ return alias.as === ref;
141
+ });
142
+ let refElement;
143
+ if (matchAlias) {
144
+ refEntity = csn.definitions[matchAlias.from];
145
+ } else {
146
+ refElement = refEntity.elements[ref];
147
+ // Mixin
148
+ if (!refElement) {
149
+ refElement = definition.elements[ref];
150
+ if (!refElement && definition.query.SELECT.mixin) {
151
+ refElement = definition.query.SELECT.mixin[ref];
152
+ if (!refElement && definition.query.SELECT.mixin[column.ref[0]]) {
153
+ refElement =
154
+ definition.query.SELECT.mixin[column.ref[0]]._target.elements[
155
+ ref
156
+ ];
157
+ }
158
+ }
159
+ }
160
+ eachCallback(refEntity, refElement);
161
+ if (
162
+ refElement.type === "cds.Association" ||
163
+ refElement.type === "cds.Composition"
164
+ ) {
165
+ refEntity = csn.definitions[refElement.target];
166
+ }
149
167
  }
150
- });
168
+ refAlias = [];
169
+ });
170
+ afterCallback(column);
171
+ }
172
+ });
151
173
  }
@@ -1,26 +1,16 @@
1
- const cdsLint = require("../../api");
2
-
3
- module.exports = cdsLint.createRule(
4
- /* Rule meta */
5
- {
1
+ module.exports = require("../../api").createRule({
2
+ meta: {
6
3
  docs: {
7
4
  description: `Checks whether the CDS model file can be compiled by the @sap/cds wihtout errors.`,
8
5
  category: "Model Validation",
9
6
  version: "1.0.0",
10
7
  },
8
+ severity: "error",
11
9
  type: "problem",
12
10
  },
13
-
14
- /* Rule logic */
15
- (report, cds) => {
16
- return createReport(report, cds) || report;
17
- }
18
- );
19
-
20
- const createReport = (report, cds) => {
21
- const m = cds.model;
22
- if (m.err) {
23
- if (m.err) {
11
+ create: function (context) {
12
+ const m = context.cds.model;
13
+ if (m && m.err) {
24
14
  // If any csn compile errors occur
25
15
  m.err.messages.forEach((err) => {
26
16
  const msg = err.message;
@@ -37,10 +27,8 @@ const createReport = (report, cds) => {
37
27
  loc.end.line = err.$location.endLine;
38
28
  file = err.$location.file;
39
29
  }
40
- // Add to ESLint report
41
- report.push({ message: `${msg}`, loc, file });
30
+ context.report({ message: `${msg}`, loc, file });
42
31
  });
43
32
  }
44
33
  }
45
- return report;
46
- };
34
+ });
@@ -1,55 +1,51 @@
1
+ const os = require("os");
1
2
  const cp = require("child_process");
2
3
  const semver = require("semver");
3
- const cdsLint = require("../../api");
4
4
 
5
- module.exports = cdsLint.createRule(
6
- // Rule meta data
7
- {
5
+ const IS_WIN = os.platform() === "win32";
6
+
7
+ module.exports = require("../../api").createRule({
8
+ meta: {
8
9
  docs: {
9
- description: "Checks whether the latest cds version is being used.",
10
+ description: "Checks whether the latest `@sap/cds` version is being used.",
10
11
  category: "Environment",
11
12
  version: "1.0.4",
12
13
  },
13
14
  type: "suggestion",
15
+ hasSuggestions: true,
14
16
  messages: {
15
- latestCDSVersion: "A newer CDS version 5.1.0 is available!",
17
+ latestCDSVersion: `A newer CDS version is available!`,
16
18
  },
17
19
  },
18
-
19
- // Rule (cds) logic
20
- (report, cds) => {
21
- return createRuleReport(report, cds) || report;
22
- }
23
- );
24
-
25
- const createRuleReport = (report, cds) => {
26
- let result;
27
- let cdsVersions;
28
- const e = cds.environment;
29
- if (!e) {
30
- try {
31
- result = cp
32
- .execSync(`npm outdated @sap/cds --json`, {
33
- cwd: process.cwd(),
34
- })
35
- .toString();
36
- cdsVersions = JSON.parse(result)["@sap/cds"];
37
- } catch (err) {
38
- // Do not throw
20
+ create: function (context) {
21
+ let result;
22
+ let cdsVersions;
23
+ const e = context.cds.environment;
24
+ if (!e) {
25
+ try {
26
+ result = cp
27
+ .execSync(`npm outdated @sap/cds --json`, {
28
+ cwd: process.cwd(),
29
+ shell: IS_WIN,
30
+ stdio: "pipe",
31
+ })
32
+ .toString();
33
+ cdsVersions = JSON.parse(result)["@sap/cds"];
34
+ } catch (err) {
35
+ // Do not throw
36
+ }
37
+ } else {
38
+ cdsVersions = context.cds.environment["@sap/cds"];
39
39
  }
40
- } else {
41
- cdsVersions = cds.environment["@sap/cds"];
42
- }
43
- // If current cds version is not the latest
44
- if (
45
- Object.keys(cdsVersions).length !== 0 &&
46
- !semver.satisfies(cdsVersions.latest, cdsVersions.current)
47
- ) {
48
- // Add to ESLint report
49
- report.push({
50
- message: `A newer CDS version ${cdsVersions.latest} is available!`,
51
- loc: { line: 0, column: 0 },
52
- });
53
- }
54
- return report;
55
- };
40
+ // If current cds version is not the latest
41
+ if (
42
+ Object.keys(cdsVersions).length !== 0 &&
43
+ !semver.satisfies(cdsVersions.latest, cdsVersions.current)
44
+ ) {
45
+ // Add to ESLint report
46
+ context.report({
47
+ messageId: "latestCDSVersion",
48
+ });
49
+ }
50
+ },
51
+ });
@@ -1,55 +1,44 @@
1
1
  const path = require("path");
2
2
  const semver = require("semver");
3
- const cdsLint = require("../../api");
4
3
 
5
- module.exports = cdsLint.createRule(
6
- /* Rule meta */
7
- {
4
+ module.exports = require("../../api").createRule({
5
+ meta: {
8
6
  docs: {
9
- description:
10
- "Checks whether the minimum node version required by the <code>@sap/cds</code> is achieved.",
7
+ description: `Checks whether the minimum Node.js version required by \`@sap/cds\` is achieved.`,
11
8
  category: "Environment",
9
+ recommended: true,
12
10
  version: "1.0.0",
13
11
  },
12
+ severity: "error",
14
13
  type: "problem",
15
14
  },
16
-
17
- /* Rule logic */
18
- (report, cds, sourcecode, filepath) => {
19
- return createReport(report, cds, filepath) || report;
20
- }
21
- );
22
-
23
- const createReport = (report, cds, filepath) => {
24
- const e = cds.environment;
25
- let nodeVersion, nodeVersionCDS;
26
- if (!e) {
27
- // Get current and required node versions
28
- try {
29
- const CDSPath = require.resolve("@sap/cds/package.json", {
30
- paths: [path.dirname(filepath)],
15
+ create: function (context) {
16
+ const e = context.cds.environment;
17
+ let nodeVersion, nodeVersionCDS;
18
+ if (!e) {
19
+ // Get current and required node versions
20
+ try {
21
+ const CDSPath = require.resolve("@sap/cds/package.json", {
22
+ paths: [path.dirname(context.filePath)],
23
+ });
24
+ const jsonCDS = require(CDSPath);
25
+ nodeVersion = process.version;
26
+ nodeVersionCDS = jsonCDS.engines.node;
27
+ } catch (err) {
28
+ // Do not throw
29
+ }
30
+ } else {
31
+ nodeVersion = context.cds.environment.nodeVersion;
32
+ nodeVersionCDS = context.cds.environment.nodeVersionCDS;
33
+ }
34
+ if (
35
+ nodeVersion &&
36
+ nodeVersionCDS &&
37
+ !semver.satisfies(nodeVersion, nodeVersionCDS, { loose: true })
38
+ ) {
39
+ context.report({
40
+ message: `CDS minimum node version of ${nodeVersionCDS} required, found ${nodeVersion}!`,
31
41
  });
32
- const jsonCDS = require(CDSPath);
33
- nodeVersion = process.version;
34
- nodeVersionCDS = jsonCDS.engines.node;
35
- } catch (err) {
36
- // Do not throw
37
42
  }
38
- } else {
39
- nodeVersion = cds.environment.nodeVersion;
40
- nodeVersionCDS = cds.environment.nodeVersionCDS;
41
- }
42
- // If required version is not satisfied
43
- if (
44
- nodeVersion &&
45
- nodeVersionCDS &&
46
- !semver.satisfies(nodeVersion, nodeVersionCDS, { loose: true })
47
- ) {
48
- // Add to CDS lint report
49
- report.push({
50
- message: `CDS minimum node version of ${nodeVersionCDS} required, found ${nodeVersion}!`,
51
- loc: { line: 0, column: 1 },
52
- });
53
- }
54
- return report;
55
- };
43
+ },
44
+ });
@@ -1,25 +1,38 @@
1
1
  module.exports = require("../../api").defineRule({
2
- meta: { docs: { description: `Avoid using reserved SQL keywords.` }},
3
- create (context) {
4
- const { db = { kind: 'sql' } } = context.cds.env.requires
5
- function _check (d) {
2
+ meta: {
3
+ docs: {
4
+ description: `Avoid using reserved SQL keywords.`,
5
+ category: "Model Validation",
6
+ recommended: true,
7
+ version: "2.1.0",
8
+ },
9
+ severity: "error"
10
+ },
11
+ create(context) {
12
+ const { db = { kind: "sql" } } = context.cds.env.requires;
13
+ function _check(d) {
6
14
  if (d.name in RESERVED) {
7
-
8
15
  // Do not blame in case of external services
9
- let srv = d._service || d.parent && d.parent._service
10
- if (srv && srv['@cds.external']) return
16
+ let srv = d._service || (d.parent && d.parent._service);
17
+ if (srv && srv["@cds.external"]) return;
11
18
 
12
19
  // Do blame
13
- return `'${d.name}' is a reserved keyword in ${db.kind.toUpperCase()}`
20
+ return `'${d.name}' is a reserved keyword in ${db.kind.toUpperCase()}`;
14
21
  }
15
22
  }
16
- return { entity: _check, element: _check }
17
- }
18
- })
23
+ return { entity: _check, element: _check };
24
+ },
25
+ });
19
26
 
20
27
  // REVISIT: Replace by compiler-provided check
21
28
  const RESERVED = {
22
- ORDER: 1, Order: 1, order: 1,
23
- GROUP: 1, Group: 1, group: 1,
24
- LIMIT: 1, Limit: 1, limit: 1,
25
- }
29
+ ORDER: 1,
30
+ Order: 1,
31
+ order: 1,
32
+ GROUP: 1,
33
+ Group: 1,
34
+ group: 1,
35
+ LIMIT: 1,
36
+ Limit: 1,
37
+ limit: 1,
38
+ };
@@ -0,0 +1,37 @@
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
+ recommended: true,
7
+ version: "2.2.1",
8
+ },
9
+ severity: "warn",
10
+ type: "suggestion",
11
+ messages: {
12
+ noJoinOnDraftEnabledEntities: `Do not use draft-enabled entities in views that make use of \`JOIN\`.`,
13
+ },
14
+ },
15
+ create: function (context) {
16
+ const m = context.cds.model; if (!m) return
17
+ m.foreach("entity", (entity) => {
18
+ if (entity["@odata.draft.enabled"]) {
19
+ if (entity.query.SELECT.from.join) {
20
+ const location = entity.query.$location;
21
+ if (context.sourcecode.lines[location.line - 1]) {
22
+ const endCol = context.sourcecode.lines[location.line - 1].length;
23
+ const loc = {
24
+ start: { line: location.line, column: location.col - 1 },
25
+ end: { line: location.line, column: endCol },
26
+ };
27
+ context.report({
28
+ messageId: "noJoinOnDraftEnabledEntities",
29
+ loc,
30
+ file: entity.$location.file,
31
+ });
32
+ }
33
+ }
34
+ }
35
+ });
36
+ },
37
+ });