@sap/eslint-plugin-cds 2.5.0 → 2.6.1
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/CHANGELOG.md +24 -0
- package/README.md +2 -1
- package/lib/api/index.js +9 -9
- package/lib/conf/all.js +20 -19
- package/lib/conf/index.js +10 -10
- package/lib/conf/recommended.js +17 -16
- package/lib/constants.js +16 -14
- package/lib/index.js +17 -11
- package/lib/parser.js +90 -82
- package/lib/rules/assoc2many-ambiguous-key.js +71 -70
- package/lib/rules/auth-no-empty-restrictions.js +16 -15
- package/lib/rules/auth-use-requires.js +19 -18
- package/lib/rules/auth-valid-restrict-grant.js +49 -46
- package/lib/rules/auth-valid-restrict-keys.js +19 -18
- package/lib/rules/auth-valid-restrict-to.js +68 -64
- package/lib/rules/auth-valid-restrict-where.js +44 -43
- package/lib/rules/extension-restrictions.js +69 -0
- package/lib/rules/index.js +23 -22
- package/lib/rules/latest-cds-version.js +21 -20
- package/lib/rules/min-node-version.js +22 -22
- package/lib/rules/no-db-keywords.js +21 -27
- package/lib/rules/no-dollar-prefixed-names.js +12 -11
- package/lib/rules/no-join-on-draft.js +27 -0
- package/lib/rules/require-2many-oncond.js +8 -8
- package/lib/rules/sql-cast-suggestion.js +13 -12
- package/lib/rules/start-elements-lowercase.js +42 -41
- package/lib/rules/start-entities-uppercase.js +26 -25
- package/lib/rules/valid-csv-header.js +58 -57
- package/lib/types.d.ts +1 -0
- package/lib/utils/Cache.js +17 -17
- package/lib/utils/Colors.js +8 -8
- package/lib/utils/createRule.js +172 -153
- package/lib/utils/findFuzzy.js +37 -38
- package/lib/utils/genDocs.js +224 -242
- package/lib/utils/getConfigPath.js +27 -27
- package/lib/utils/getConfiguredFileTypes.js +4 -4
- package/lib/utils/getFileExtensions.js +3 -3
- package/lib/utils/getProjectRootPath.js +25 -0
- package/lib/utils/isConfiguredFileType.js +11 -11
- package/lib/utils/rules.js +59 -59
- package/lib/utils/runRuleTester.js +76 -71
- package/package.json +7 -1
- package/lib/rules/no-join-on-draft-enabled-entities.js +0 -25
- package/lib/utils/createRuleDocs.js +0 -361
- package/lib/utils/jsonc.js +0 -1
- package/lib/utils/jsoncParser.js +0 -1
|
@@ -1,130 +1,134 @@
|
|
|
1
|
-
const { isEmptyString, isStringInArray, findFuzzy,
|
|
1
|
+
const { isEmptyString, isStringInArray, findFuzzy, isEmptyObject } = require('../utils/rules')
|
|
2
2
|
|
|
3
|
-
const VALID_PSEUDO_ROLES = [
|
|
3
|
+
const VALID_PSEUDO_ROLES = ['authenticated-user', 'system-user', 'any']
|
|
4
4
|
|
|
5
5
|
module.exports = {
|
|
6
6
|
meta: {
|
|
7
|
+
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
7
8
|
docs: {
|
|
8
|
-
description:
|
|
9
|
-
category:
|
|
9
|
+
description: '`@restrict.to` must have valid values.',
|
|
10
|
+
category: 'Model Validation',
|
|
10
11
|
recommended: true
|
|
11
12
|
},
|
|
12
13
|
hasSuggestions: true,
|
|
13
14
|
messages: {
|
|
14
|
-
InvalidItem:
|
|
15
|
-
ReplaceItemWith:
|
|
15
|
+
InvalidItem: "Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?",
|
|
16
|
+
ReplaceItemWith: "Replace '{{invalid}}' with '{{candidates}}'"
|
|
16
17
|
},
|
|
17
|
-
type:
|
|
18
|
-
model:
|
|
18
|
+
type: 'problem',
|
|
19
|
+
model: 'inferred'
|
|
19
20
|
},
|
|
20
|
-
create(context) {
|
|
21
|
+
create (context) {
|
|
21
22
|
return {
|
|
22
|
-
entity:
|
|
23
|
-
}
|
|
23
|
+
entity: checkRestrictTo
|
|
24
|
+
}
|
|
24
25
|
|
|
25
|
-
function
|
|
26
|
-
const USER_ROLES = []
|
|
27
|
-
const model = context.getModel()
|
|
26
|
+
function checkRestrictTo (e) {
|
|
27
|
+
const USER_ROLES = []
|
|
28
|
+
const model = context.getModel()
|
|
28
29
|
|
|
29
|
-
model.foreach(
|
|
30
|
-
if (e[
|
|
31
|
-
e[
|
|
30
|
+
model.foreach('entity', (e) => {
|
|
31
|
+
if (e['@restrict']) {
|
|
32
|
+
e['@restrict'].forEach((p) => {
|
|
32
33
|
if (p.to) {
|
|
33
34
|
switch (typeof p.to) {
|
|
34
|
-
case
|
|
35
|
+
case 'string':
|
|
35
36
|
if (p.to !== p.to.toLowerCase() && !USER_ROLES.includes(p.to)) {
|
|
36
|
-
USER_ROLES.push(p.to)
|
|
37
|
+
USER_ROLES.push(p.to)
|
|
37
38
|
}
|
|
38
|
-
break
|
|
39
|
-
case
|
|
39
|
+
break
|
|
40
|
+
case 'object':
|
|
40
41
|
for (const r in p.to) {
|
|
41
42
|
if (r !== r.toLowerCase() && !USER_ROLES.includes(r)) {
|
|
42
|
-
USER_ROLES.push(r)
|
|
43
|
+
USER_ROLES.push(r)
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
|
-
})
|
|
48
|
+
})
|
|
48
49
|
}
|
|
49
|
-
})
|
|
50
|
-
const ROLES = USER_ROLES.concat(VALID_PSEUDO_ROLES)
|
|
50
|
+
})
|
|
51
|
+
const ROLES = USER_ROLES.concat(VALID_PSEUDO_ROLES)
|
|
51
52
|
|
|
52
|
-
if (e[
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const { prefix } = splitEntityName(e);
|
|
56
|
-
const prefixSplit = prefix.split(".");
|
|
57
|
-
const serviceName = prefixSplit[prefixSplit.length - 1];
|
|
58
|
-
let services = model.services;
|
|
53
|
+
if (e['@restrict']) {
|
|
54
|
+
const node = context.getNode(e)
|
|
55
|
+
const file = e.$location.file
|
|
59
56
|
|
|
60
57
|
// TODO: For hierachies, check whether service restriction exists
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
58
|
+
// const { prefix } = splitDefName(e)
|
|
59
|
+
// const prefixSplit = prefix.split('.')
|
|
60
|
+
// const serviceName = prefixSplit[prefixSplit.length - 1]
|
|
61
|
+
// const services = model.services
|
|
62
|
+
// let grantAllTo;
|
|
63
|
+
// Object.values(services).map((s) => {
|
|
64
|
+
// if (s.name === serviceName && s['@requires']) {
|
|
65
|
+
// grantAllTo = s['@requires'];
|
|
66
|
+
// }
|
|
67
|
+
// })
|
|
67
68
|
|
|
68
|
-
for (const entry of e[
|
|
69
|
-
if (Object.keys(entry).includes(
|
|
70
|
-
const toValue = entry.to
|
|
69
|
+
for (const entry of e['@restrict']) {
|
|
70
|
+
if (Object.keys(entry).includes('to')) {
|
|
71
|
+
const toValue = entry.to
|
|
71
72
|
|
|
72
73
|
switch (typeof toValue) {
|
|
73
|
-
case
|
|
74
|
+
case 'string': {
|
|
74
75
|
if (isEmptyString(toValue)) {
|
|
75
76
|
context.report({
|
|
76
77
|
message: `Missing role on ${e.name} for \`@restrict.to\`.`,
|
|
77
78
|
node,
|
|
78
79
|
file
|
|
79
|
-
})
|
|
80
|
+
})
|
|
80
81
|
} else {
|
|
81
|
-
const isPseudoRole = entry.to && entry.to === entry.to.toLowerCase()
|
|
82
|
+
const isPseudoRole = entry.to && entry.to === entry.to.toLowerCase()
|
|
82
83
|
if (!isStringInArray(toValue, ROLES, isPseudoRole)) {
|
|
83
|
-
const candidates = findFuzzy(toValue, ROLES.sort())
|
|
84
|
+
const candidates = findFuzzy(toValue, ROLES.sort())
|
|
84
85
|
context.report({
|
|
85
|
-
messageId:
|
|
86
|
+
messageId: 'InvalidItem',
|
|
86
87
|
data: { invalid: toValue, candidates },
|
|
87
88
|
node,
|
|
88
89
|
file
|
|
89
|
-
})
|
|
90
|
+
})
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
|
-
break
|
|
93
|
+
break
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
case
|
|
96
|
+
case 'object':
|
|
96
97
|
if (isEmptyObject(toValue)) {
|
|
97
98
|
context.report({
|
|
98
99
|
message: `Missing roles on ${e.name} for \`@restrict.to\`.`,
|
|
99
100
|
node,
|
|
100
101
|
file
|
|
101
|
-
})
|
|
102
|
+
})
|
|
102
103
|
} else {
|
|
103
|
-
|
|
104
|
+
// If values contain 'any', 'any' only is enough
|
|
105
|
+
if (toValue.length > 1 && toValue.includes('any')) {
|
|
104
106
|
context.report({
|
|
105
|
-
messageId:
|
|
106
|
-
data: { invalid: `[${toValue}]`, candidates: [
|
|
107
|
+
messageId: 'InvalidItem',
|
|
108
|
+
data: { invalid: `[${toValue}]`, candidates: ['["any"]'] },
|
|
107
109
|
node,
|
|
108
110
|
file
|
|
109
|
-
})
|
|
111
|
+
})
|
|
110
112
|
}
|
|
111
113
|
toValue.forEach((value) => {
|
|
112
114
|
if (!ROLES.includes(value)) {
|
|
113
|
-
const candidates = findFuzzy(value, ROLES.sort())
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
115
|
+
const candidates = findFuzzy(value, ROLES.sort(), undefined, false, 2)
|
|
116
|
+
if (candidates.length > 0) {
|
|
117
|
+
context.report({
|
|
118
|
+
messageId: 'InvalidItem',
|
|
119
|
+
data: { invalid: value, candidates },
|
|
120
|
+
node,
|
|
121
|
+
file
|
|
122
|
+
})
|
|
123
|
+
}
|
|
120
124
|
}
|
|
121
|
-
})
|
|
125
|
+
})
|
|
122
126
|
}
|
|
123
|
-
break
|
|
127
|
+
break
|
|
124
128
|
}
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
133
|
}
|
|
130
|
-
}
|
|
134
|
+
}
|
|
@@ -1,83 +1,84 @@
|
|
|
1
|
-
const cds = require(
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
2
|
|
|
3
|
-
const VALID_PSEUDO_ROLES = [
|
|
3
|
+
const VALID_PSEUDO_ROLES = ['authenticated-user', 'system-user', 'any']
|
|
4
4
|
|
|
5
5
|
module.exports = {
|
|
6
6
|
meta: {
|
|
7
|
+
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
7
8
|
docs: {
|
|
8
|
-
description:
|
|
9
|
-
category:
|
|
9
|
+
description: '`@restrict.where` must have valid values.',
|
|
10
|
+
category: 'Model Validation',
|
|
10
11
|
recommended: true
|
|
11
12
|
},
|
|
12
|
-
severity:
|
|
13
|
+
severity: 'error',
|
|
13
14
|
hasSuggestions: true,
|
|
14
15
|
messages: {
|
|
15
|
-
InvalidItem:
|
|
16
|
-
ReplaceItemWith:
|
|
16
|
+
InvalidItem: "Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?",
|
|
17
|
+
ReplaceItemWith: "Replace '{{invalid}}' with '{{candidates}}'"
|
|
17
18
|
},
|
|
18
|
-
type:
|
|
19
|
-
model:
|
|
19
|
+
type: 'problem',
|
|
20
|
+
model: 'inferred'
|
|
20
21
|
},
|
|
21
|
-
create(context) {
|
|
22
|
-
const model = context.getModel()
|
|
22
|
+
create (context) {
|
|
23
|
+
const model = context.getModel()
|
|
23
24
|
|
|
24
25
|
return {
|
|
25
|
-
entity:
|
|
26
|
-
}
|
|
26
|
+
entity: checkRestrictGrant
|
|
27
|
+
}
|
|
27
28
|
|
|
28
|
-
function
|
|
29
|
-
const USER_ROLES = []
|
|
29
|
+
function checkRestrictGrant (e) {
|
|
30
|
+
const USER_ROLES = []
|
|
30
31
|
|
|
31
|
-
model.foreach(
|
|
32
|
-
if (e[
|
|
33
|
-
e[
|
|
32
|
+
model.foreach('entity', (e) => {
|
|
33
|
+
if (e['@restrict']) {
|
|
34
|
+
e['@restrict'].forEach((p) => {
|
|
34
35
|
if (p.to) {
|
|
35
36
|
switch (typeof p.to) {
|
|
36
|
-
case
|
|
37
|
+
case 'string':
|
|
37
38
|
if (p.to !== p.to.toLowerCase() && !USER_ROLES.includes(p.to)) {
|
|
38
|
-
USER_ROLES.push(p.to)
|
|
39
|
+
USER_ROLES.push(p.to)
|
|
39
40
|
}
|
|
40
|
-
break
|
|
41
|
-
case
|
|
41
|
+
break
|
|
42
|
+
case 'object':
|
|
42
43
|
for (const r in p.to) {
|
|
43
44
|
if (r !== r.toLowerCase() && !USER_ROLES.includes(r)) {
|
|
44
|
-
USER_ROLES.push(r)
|
|
45
|
+
USER_ROLES.push(r)
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
|
-
})
|
|
50
|
+
})
|
|
50
51
|
}
|
|
51
|
-
})
|
|
52
|
-
const ROLES = USER_ROLES.concat(VALID_PSEUDO_ROLES)
|
|
52
|
+
})
|
|
53
|
+
const ROLES = USER_ROLES.concat(VALID_PSEUDO_ROLES)
|
|
53
54
|
|
|
54
|
-
if (e[
|
|
55
|
-
const node = context.getNode(e)
|
|
56
|
-
const file = e.$location.file
|
|
57
|
-
for (const entry of e[
|
|
58
|
-
const whereValues = entry.where
|
|
59
|
-
if (whereValues && typeof whereValues ===
|
|
60
|
-
let cxn
|
|
55
|
+
if (e['@restrict']) {
|
|
56
|
+
const node = context.getNode(e)
|
|
57
|
+
const file = e.$location.file
|
|
58
|
+
for (const entry of e['@restrict']) {
|
|
59
|
+
const whereValues = entry.where
|
|
60
|
+
if (whereValues && typeof whereValues === 'string') {
|
|
61
|
+
let cxn
|
|
61
62
|
try {
|
|
62
|
-
cxn = cds.parse.expr(entry.where)
|
|
63
|
+
cxn = cds.parse.expr(entry.where)
|
|
63
64
|
} catch (err) {
|
|
64
65
|
context.report({
|
|
65
|
-
message:
|
|
66
|
+
message: 'Invalid `where` expression, CDS compilation failed.',
|
|
66
67
|
node,
|
|
67
68
|
file
|
|
68
|
-
})
|
|
69
|
+
})
|
|
69
70
|
}
|
|
70
71
|
if (cxn && cxn.xpr) {
|
|
71
|
-
const operator = cxn.xpr[1]
|
|
72
|
-
const role = cxn.xpr[2].ref
|
|
73
|
-
if (operator ===
|
|
74
|
-
const isValidRole = role
|
|
72
|
+
const operator = cxn.xpr[1]
|
|
73
|
+
const role = cxn.xpr[2].ref
|
|
74
|
+
if (operator === '=') {
|
|
75
|
+
const isValidRole = role === '$user' || ROLES.includes(role)
|
|
75
76
|
if (!isValidRole) {
|
|
76
77
|
context.report({
|
|
77
|
-
message: `Invalid \`where\` expression, role
|
|
78
|
+
message: `Invalid \`where\` expression, role ${role} not found.`,
|
|
78
79
|
node,
|
|
79
80
|
file
|
|
80
|
-
})
|
|
81
|
+
})
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
}
|
|
@@ -86,4 +87,4 @@ module.exports = {
|
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
|
-
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const cds = requireMtx()
|
|
2
|
+
|
|
3
|
+
const { dirname } = require('path')
|
|
4
|
+
|
|
5
|
+
const rule = module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'Extensions must not violate restrictions set by the extended SaaS app.',
|
|
9
|
+
category: 'Model Validation',
|
|
10
|
+
recommended: true
|
|
11
|
+
},
|
|
12
|
+
hasSuggestions: false,
|
|
13
|
+
type: 'problem',
|
|
14
|
+
model: 'parsed'
|
|
15
|
+
},
|
|
16
|
+
mtxApi: () => {
|
|
17
|
+
if (!cds) return // no mtxs lib installed
|
|
18
|
+
if (!cds.xt?.linter) return // too old mtxs
|
|
19
|
+
return cds.xt.linter
|
|
20
|
+
},
|
|
21
|
+
create (context) {
|
|
22
|
+
if (!rule.mtxApi()) return // no mtxs lib installed
|
|
23
|
+
|
|
24
|
+
return () => {
|
|
25
|
+
const extModel = context.getModel()
|
|
26
|
+
if (!extModel) return
|
|
27
|
+
|
|
28
|
+
const base = baseModel(context)
|
|
29
|
+
if (!base) return
|
|
30
|
+
|
|
31
|
+
const file = context.getFilename()
|
|
32
|
+
const findings = rule.mtxApi().lint(extModel, base.model, base.env)
|
|
33
|
+
for (const finding of findings) {
|
|
34
|
+
context.report({
|
|
35
|
+
message: finding.message,
|
|
36
|
+
node: context.getNode(finding.element),
|
|
37
|
+
file
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function baseModel (context) {
|
|
45
|
+
let dir = context.getFilename()
|
|
46
|
+
do {
|
|
47
|
+
dir = dirname(dir)
|
|
48
|
+
const projEnv = cds.env.for('cds', dir)
|
|
49
|
+
if (!projEnv.extends) continue
|
|
50
|
+
const files = cds.resolve(projEnv.extends, { root: dir }) // resolve local base model
|
|
51
|
+
if (files?.length) {
|
|
52
|
+
const model = cds.load(files, { sync: true })
|
|
53
|
+
// env of SaaS app is needed for extension rules
|
|
54
|
+
const env = cds.env.for('cds', dirname(files[0]))
|
|
55
|
+
return { env, model }
|
|
56
|
+
}
|
|
57
|
+
} while (dir && dir !== cds.root && dir.length > 1)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @returns { import('@sap/cds/apis/cds') } */
|
|
61
|
+
function requireMtx () {
|
|
62
|
+
const cds = require('@sap/cds')
|
|
63
|
+
try {
|
|
64
|
+
const pkg = require.resolve('@sap/cds-mtxs', { paths: [cds.root, __dirname] })
|
|
65
|
+
return require(pkg)
|
|
66
|
+
} catch (e) {
|
|
67
|
+
if (e.code !== 'MODULE_NOT_FOUND') throw e
|
|
68
|
+
}
|
|
69
|
+
}
|
package/lib/rules/index.js
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
const Cache = require(
|
|
2
|
-
const createRule = require(
|
|
1
|
+
const Cache = require('../utils/Cache')
|
|
2
|
+
const { createRule } = require('../api')
|
|
3
3
|
|
|
4
4
|
const rules = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
'assoc2many-ambiguous-key': () => createRule(require('./assoc2many-ambiguous-key')),
|
|
6
|
+
'auth-no-empty-restrictions': () => createRule(require('./auth-no-empty-restrictions')),
|
|
7
|
+
'auth-use-requires': () => createRule(require('./auth-use-requires')),
|
|
8
|
+
'auth-valid-restrict-grant': () => createRule(require('./auth-valid-restrict-grant')),
|
|
9
|
+
'auth-valid-restrict-keys': () => createRule(require('./auth-valid-restrict-keys')),
|
|
10
|
+
'auth-valid-restrict-to': () => createRule(require('./auth-valid-restrict-to')),
|
|
11
|
+
'auth-valid-restrict-where': () => createRule(require('./auth-valid-restrict-where')),
|
|
12
|
+
'latest-cds-version': () => createRule(require('./latest-cds-version')),
|
|
13
|
+
'min-node-version': () => createRule(require('./min-node-version')),
|
|
14
|
+
'no-db-keywords': () => createRule(require('./no-db-keywords')),
|
|
15
|
+
'no-dollar-prefixed-names': () => createRule(require('./no-dollar-prefixed-names')),
|
|
16
|
+
'no-join-on-draft': () => createRule(require('./no-join-on-draft')),
|
|
17
|
+
'require-2many-oncond': () => createRule(require('./require-2many-oncond')),
|
|
18
|
+
'sql-cast-suggestion': () => createRule(require('./sql-cast-suggestion')),
|
|
19
|
+
'start-elements-lowercase': () => createRule(require('./start-elements-lowercase')),
|
|
20
|
+
'start-entities-uppercase': () => createRule(require('./start-entities-uppercase')),
|
|
21
|
+
'valid-csv-header': () => createRule(require('./valid-csv-header')),
|
|
22
|
+
'extension-restrictions': () => createRule(require('./extension-restrictions'))
|
|
23
|
+
}
|
|
23
24
|
|
|
24
|
-
Cache.set(
|
|
25
|
+
Cache.set('rules', rules)
|
|
25
26
|
|
|
26
|
-
module.exports = rules
|
|
27
|
+
module.exports = rules
|
|
@@ -1,43 +1,44 @@
|
|
|
1
|
-
const cp = require(
|
|
2
|
-
const semver = require(
|
|
1
|
+
const cp = require('child_process')
|
|
2
|
+
const semver = require('semver')
|
|
3
3
|
|
|
4
4
|
module.exports = {
|
|
5
5
|
meta: {
|
|
6
|
+
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
6
7
|
docs: {
|
|
7
|
-
description:
|
|
8
|
+
description: 'Checks whether the latest `@sap/cds` version is being used.'
|
|
8
9
|
},
|
|
9
|
-
type:
|
|
10
|
+
type: 'suggestion',
|
|
10
11
|
hasSuggestions: true,
|
|
11
12
|
messages: {
|
|
12
|
-
latestCDSVersion:
|
|
13
|
+
latestCDSVersion: 'A newer CDS version is available!'
|
|
13
14
|
},
|
|
14
|
-
severity:
|
|
15
|
-
model:
|
|
15
|
+
severity: 'off',
|
|
16
|
+
model: 'none'
|
|
16
17
|
},
|
|
17
18
|
create: function (context) {
|
|
18
|
-
return
|
|
19
|
+
return checkLatestCdsVersion
|
|
19
20
|
|
|
20
|
-
function
|
|
21
|
-
let cdsVersions
|
|
22
|
-
let e = context.getEnvironment()
|
|
21
|
+
function checkLatestCdsVersion () {
|
|
22
|
+
let cdsVersions
|
|
23
|
+
let e = context.getEnvironment()
|
|
23
24
|
if (!e) {
|
|
24
25
|
e = cp
|
|
25
|
-
.execSync(
|
|
26
|
+
.execSync('npm outdated @sap/cds --json', {
|
|
26
27
|
cwd: process.cwd(),
|
|
27
|
-
stdio:
|
|
28
|
+
stdio: 'pipe'
|
|
28
29
|
})
|
|
29
|
-
.toString()
|
|
30
|
+
.toString()
|
|
30
31
|
}
|
|
31
|
-
if (e && e[
|
|
32
|
-
cdsVersions = e[
|
|
32
|
+
if (e && e['@sap/cds']) {
|
|
33
|
+
cdsVersions = e['@sap/cds']
|
|
33
34
|
// If current cds version is not the latest
|
|
34
35
|
if (Object.keys(cdsVersions).length !== 0 && !semver.satisfies(cdsVersions.latest, cdsVersions.current)) {
|
|
35
36
|
context.report({
|
|
36
|
-
messageId:
|
|
37
|
+
messageId: 'latestCDSVersion',
|
|
37
38
|
node: context.getNode()
|
|
38
|
-
})
|
|
39
|
+
})
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,36 +1,37 @@
|
|
|
1
|
-
const path = require(
|
|
2
|
-
const semver = require(
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const semver = require('semver')
|
|
3
3
|
|
|
4
4
|
module.exports = {
|
|
5
5
|
meta: {
|
|
6
|
+
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
6
7
|
docs: {
|
|
7
|
-
description:
|
|
8
|
+
description: 'Checks whether the minimum Node.js version required by `@sap/cds` is achieved.'
|
|
8
9
|
},
|
|
9
|
-
severity:
|
|
10
|
-
type:
|
|
11
|
-
model:
|
|
10
|
+
severity: 'off',
|
|
11
|
+
type: 'problem',
|
|
12
|
+
model: 'none'
|
|
12
13
|
},
|
|
13
14
|
create: function (context) {
|
|
14
|
-
return
|
|
15
|
+
return checkMinNodeVersion
|
|
15
16
|
|
|
16
|
-
function
|
|
17
|
-
const e = context.getEnvironment()
|
|
18
|
-
let nodeVersion, nodeVersionCDS
|
|
17
|
+
function checkMinNodeVersion () {
|
|
18
|
+
const e = context.getEnvironment()
|
|
19
|
+
let nodeVersion, nodeVersionCDS
|
|
19
20
|
if (!e) {
|
|
20
21
|
// Get current and required node versions
|
|
21
22
|
try {
|
|
22
|
-
const CDSPath = require.resolve(
|
|
23
|
-
paths: [path.dirname(
|
|
24
|
-
})
|
|
25
|
-
const jsonCDS = require(CDSPath)
|
|
26
|
-
nodeVersion = process.version
|
|
27
|
-
nodeVersionCDS = jsonCDS.engines.node
|
|
23
|
+
const CDSPath = require.resolve('@sap/cds/package.json', {
|
|
24
|
+
paths: [path.dirname('.')]
|
|
25
|
+
})
|
|
26
|
+
const jsonCDS = require(CDSPath)
|
|
27
|
+
nodeVersion = process.version
|
|
28
|
+
nodeVersionCDS = jsonCDS.engines.node
|
|
28
29
|
} catch (err) {
|
|
29
30
|
// Do not throw
|
|
30
31
|
}
|
|
31
32
|
} else {
|
|
32
|
-
nodeVersion = e.nodeVersion
|
|
33
|
-
nodeVersionCDS = e.nodeVersionCDS
|
|
33
|
+
nodeVersion = e.nodeVersion
|
|
34
|
+
nodeVersionCDS = e.nodeVersionCDS
|
|
34
35
|
}
|
|
35
36
|
if (
|
|
36
37
|
nodeVersion &&
|
|
@@ -40,9 +41,8 @@ module.exports = {
|
|
|
40
41
|
context.report({
|
|
41
42
|
message: `CDS minimum node version of ${nodeVersionCDS} required, found ${nodeVersion}!`,
|
|
42
43
|
node: context.getNode()
|
|
43
|
-
})
|
|
44
|
+
})
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -1,45 +1,39 @@
|
|
|
1
|
-
const cds = require(
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
4
|
meta: {
|
|
5
|
+
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
5
6
|
docs: {
|
|
6
|
-
description:
|
|
7
|
+
description: 'Avoid using reserved SQL keywords.',
|
|
7
8
|
recommended: true
|
|
8
9
|
},
|
|
9
|
-
type:
|
|
10
|
+
type: 'problem',
|
|
11
|
+
model: 'inferred'
|
|
10
12
|
},
|
|
11
|
-
create(context) {
|
|
12
|
-
const { db = { kind:
|
|
13
|
+
create (context) {
|
|
14
|
+
const { db = { kind: 'sql' } } = cds.env.requires
|
|
13
15
|
|
|
14
16
|
return {
|
|
15
|
-
|
|
16
|
-
entity:
|
|
17
|
-
element:
|
|
18
|
-
}
|
|
17
|
+
// > return standard eslint visitor callbacks registered to CSN kinds, types, ...
|
|
18
|
+
entity: checkNameIsNotReserved,
|
|
19
|
+
element: checkNameIsNotReserved
|
|
20
|
+
}
|
|
19
21
|
|
|
20
|
-
function
|
|
21
|
-
if (d.name
|
|
22
|
+
function checkNameIsNotReserved (d) {
|
|
23
|
+
if (RESERVED.includes(d.name.toUpperCase())) {
|
|
22
24
|
// Do not blame in case of external services
|
|
23
|
-
|
|
24
|
-
if (srv && srv[
|
|
25
|
+
const srv = d._service || (d.parent && d.parent._service)
|
|
26
|
+
if (srv && srv['@cds.external']) return
|
|
27
|
+
if (d.kind === 'entity' && d['@cds.persistence.skip'] === true) return
|
|
25
28
|
context.report({
|
|
26
29
|
message: `'${d.name}' is a reserved keyword in ${db.kind.toUpperCase()}`,
|
|
27
|
-
node: context.getNode(d)
|
|
28
|
-
|
|
30
|
+
node: context.getNode(d),
|
|
31
|
+
file: d.$location.file
|
|
32
|
+
})
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
|
-
}
|
|
36
|
+
}
|
|
33
37
|
|
|
34
38
|
// REVISIT: Replace by compiler-provided check
|
|
35
|
-
const RESERVED =
|
|
36
|
-
ORDER: 1,
|
|
37
|
-
Order: 1,
|
|
38
|
-
order: 1,
|
|
39
|
-
GROUP: 1,
|
|
40
|
-
Group: 1,
|
|
41
|
-
group: 1,
|
|
42
|
-
LIMIT: 1,
|
|
43
|
-
Limit: 1,
|
|
44
|
-
limit: 1
|
|
45
|
-
};
|
|
39
|
+
const RESERVED = cds.compile.to.sql.sqlite ? cds.compile.to.sql.sqlite.keywords : ['ORDER', 'GROUP', 'LIMIT']
|