@sap/eslint-plugin-cds 2.4.1 → 2.5.0
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 +16 -2
- package/lib/api/index.js +9 -8
- package/lib/conf/all.js +21 -0
- package/lib/conf/index.js +22 -0
- package/lib/conf/recommended.js +18 -0
- package/lib/constants.js +10 -8
- package/lib/index.js +11 -32
- package/lib/parser.js +158 -7
- package/lib/rules/assoc2many-ambiguous-key.js +21 -38
- package/lib/rules/auth-no-empty-restrictions.js +13 -21
- package/lib/rules/auth-use-requires.js +15 -15
- package/lib/rules/auth-valid-restrict-grant.js +71 -34
- package/lib/rules/auth-valid-restrict-keys.js +22 -16
- package/lib/rules/auth-valid-restrict-to.js +71 -27
- package/lib/rules/auth-valid-restrict-where.js +24 -15
- package/lib/rules/index.js +26 -0
- package/lib/rules/latest-cds-version.js +8 -7
- package/lib/rules/min-node-version.js +10 -9
- package/lib/rules/no-db-keywords.js +16 -15
- package/lib/rules/no-dollar-prefixed-names.js +9 -7
- package/lib/rules/no-join-on-draft-enabled-entities.js +9 -24
- package/lib/rules/require-2many-oncond.js +9 -14
- package/lib/rules/sql-cast-suggestion.js +6 -20
- package/lib/rules/start-elements-lowercase.js +5 -8
- package/lib/rules/start-entities-uppercase.js +8 -11
- package/lib/rules/valid-csv-header.js +66 -66
- package/lib/{api/lint.d.ts → types.d.ts} +4 -7
- package/lib/utils/Cache.js +33 -0
- package/lib/utils/Colors.js +9 -0
- package/lib/utils/createRule.js +304 -0
- package/lib/utils/createRuleDocs.js +361 -0
- package/lib/utils/{fuzzySearch.js → findFuzzy.js} +0 -2
- package/lib/utils/genDocs.js +363 -0
- package/lib/utils/getConfigPath.js +33 -0
- package/lib/utils/getConfiguredFileTypes.js +10 -0
- package/lib/utils/getFileExtensions.js +8 -0
- package/lib/utils/isConfiguredFileType.js +20 -0
- package/lib/utils/jsonc.js +1 -1
- package/lib/utils/jsoncParser.js +1 -0
- package/lib/utils/rules.js +112 -1041
- package/lib/utils/runRuleTester.js +111 -0
- package/package.json +4 -4
- package/lib/processor.js +0 -50
- package/lib/utils/helpers.js +0 -94
- package/lib/utils/model.js +0 -393
- package/lib/utils/ruleHelpers.js +0 -199
- package/lib/utils/ruleTester.js +0 -78
- package/lib/utils/validate.js +0 -36
package/CHANGELOG.md
CHANGED
|
@@ -6,14 +6,28 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
8
8
|
|
|
9
|
+
## [2.5.0] - 2022-08-04
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Model Validation rules use `parsed` flavor by default (`meta.model` property)
|
|
14
|
+
- Environment rules have { model: "none" }
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Flavor in `model` property on `meta` object of rule
|
|
19
|
+
- Context function `getNode()` returns Node with proper location
|
|
20
|
+
|
|
9
21
|
## [2.4.1] - 2022-06-17
|
|
10
22
|
|
|
11
|
-
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Authorization rules 'auth-*'.
|
|
12
26
|
|
|
13
27
|
### Changed
|
|
14
28
|
|
|
15
29
|
- Node.js 14 is now the minimum required Node.js version. Version 12 is no longer supported.
|
|
16
|
-
|
|
30
|
+
- Default CSN flavor in rules is `parsed`.
|
|
17
31
|
|
|
18
32
|
## [2.4.0] - 2022-04-14
|
|
19
33
|
|
package/lib/api/index.js
CHANGED
|
@@ -4,22 +4,23 @@
|
|
|
4
4
|
* Our custom ESLint plugin API should:
|
|
5
5
|
* - Expose 'createRule' and 'runRuleTester' to
|
|
6
6
|
* support the addition of *custom* CDS Lint rules at runtime
|
|
7
|
-
* - Expose '
|
|
7
|
+
* - Expose 'getFileExtensions', and 'genDocs' for usage in
|
|
8
8
|
* 'cds lint' client (@sap/cds-dk)
|
|
9
9
|
* - Expose 'parserPath' for CDS Lint rule unit tests with ESLint's ruleTester
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
12
|
+
const runRuleTester = require("../utils/runRuleTester");
|
|
13
|
+
const createRule = require("../utils/createRule");
|
|
14
|
+
const genDocs = require("../utils/genDocs");
|
|
15
|
+
const getConfigPath = require("../utils/getConfigPath");
|
|
16
|
+
const getConfiguredFileTypes = require("../utils/getConfiguredFileTypes");
|
|
16
17
|
const parserPath = require.resolve("../parser");
|
|
17
18
|
|
|
18
19
|
module.exports = {
|
|
19
|
-
createRule,
|
|
20
20
|
runRuleTester,
|
|
21
|
-
|
|
22
|
-
getFileExtensions,
|
|
21
|
+
createRule,
|
|
23
22
|
genDocs,
|
|
23
|
+
getConfigPath,
|
|
24
|
+
getFileExtensions: getConfiguredFileTypes,
|
|
24
25
|
parserPath,
|
|
25
26
|
};
|
package/lib/conf/all.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
"@sap/cds/assoc2many-ambiguous-key": 2,
|
|
5
|
+
"@sap/cds/auth-no-empty-restrictions": 2,
|
|
6
|
+
"@sap/cds/auth-use-requires": 1,
|
|
7
|
+
"@sap/cds/auth-valid-restrict-grant": 1,
|
|
8
|
+
"@sap/cds/auth-valid-restrict-keys": 1,
|
|
9
|
+
"@sap/cds/auth-valid-restrict-to": "warn",
|
|
10
|
+
"@sap/cds/auth-valid-restrict-where": "warn",
|
|
11
|
+
"@sap/cds/latest-cds-version": "warn",
|
|
12
|
+
"@sap/cds/min-node-version": "error",
|
|
13
|
+
"@sap/cds/no-db-keywords": "error",
|
|
14
|
+
"@sap/cds/no-dollar-prefixed-names": "warn",
|
|
15
|
+
"@sap/cds/no-join-on-draft-enabled-entities": "warn",
|
|
16
|
+
"@sap/cds/require-2many-oncond": "error",
|
|
17
|
+
"@sap/cds/sql-cast-suggestion": "warn",
|
|
18
|
+
"@sap/cds/start-elements-uppercase": "warn",
|
|
19
|
+
"@sap/cds/start-entities-uppercase": "warn",
|
|
20
|
+
"@sap/cds/valid-csv-header": "error"
|
|
21
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const { FILES, GLOBALS, PLUGIN_NAME } = require("../constants");
|
|
2
|
+
const { parserPath } = require("../api");
|
|
3
|
+
|
|
4
|
+
function _createConfig(config) {
|
|
5
|
+
return {
|
|
6
|
+
root: true,
|
|
7
|
+
globals: GLOBALS,
|
|
8
|
+
plugins: [PLUGIN_NAME],
|
|
9
|
+
overrides: [
|
|
10
|
+
{
|
|
11
|
+
files: FILES,
|
|
12
|
+
parser: parserPath,
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
rules: config,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
all: _createConfig(require("./all")),
|
|
21
|
+
recommended: _createConfig(require("./recommended")),
|
|
22
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
"@sap/cds/assoc2many-ambiguous-key": [2, "hide"],
|
|
5
|
+
"@sap/cds/auth-no-empty-restrictions": 2,
|
|
6
|
+
"@sap/cds/auth-use-requires": 1,
|
|
7
|
+
"@sap/cds/auth-valid-restrict-grant": 1,
|
|
8
|
+
"@sap/cds/auth-valid-restrict-keys": 1,
|
|
9
|
+
"@sap/cds/auth-valid-restrict-to": 1,
|
|
10
|
+
"@sap/cds/auth-valid-restrict-where": 1,
|
|
11
|
+
"@sap/cds/min-node-version": 2,
|
|
12
|
+
"@sap/cds/no-db-keywords": 2,
|
|
13
|
+
"@sap/cds/no-dollar-prefixed-names": 1,
|
|
14
|
+
"@sap/cds/no-join-on-draft-enabled-entities": [1, "hide"],
|
|
15
|
+
"@sap/cds/require-2many-oncond": 2,
|
|
16
|
+
"@sap/cds/sql-cast-suggestion": 1,
|
|
17
|
+
"@sap/cds/valid-csv-header": 1
|
|
18
|
+
};
|
package/lib/constants.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* This file is used to store/share any constants for this plugin:
|
|
3
|
-
* - RULE_CATEGORIES: List of plugin's rule categories (used purely as documentation labels)
|
|
4
3
|
* - DEFAULT_RULE_CATEGORY: Default rule category (must be one of plugin's rule categories)
|
|
5
4
|
* - DEFAULT_RULE_SEVERITY: Default rule severity (must be one of those define by ESLint):
|
|
6
5
|
* https://eslint.org/docs/user-guide/configuring/rules#configuring-rules
|
|
@@ -13,14 +12,17 @@
|
|
|
13
12
|
* - GLOBALS: Globals which should be exposed to ESLint by this plugin
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
|
-
const
|
|
17
|
-
|
|
15
|
+
const RULE_FLAVORS = ["parsed" , "inferred"];
|
|
16
|
+
|
|
17
|
+
const DEFAULT_RULE_FLAVOR = RULE_FLAVORS[0];
|
|
18
|
+
|
|
18
19
|
const DEFAULT_RULE_SEVERITY = "error";
|
|
19
20
|
const DEFAULT_RULE_TYPE = "problem";
|
|
20
21
|
|
|
21
22
|
const PLUGIN_NAME = require("../package.json").name;
|
|
22
|
-
|
|
23
|
-
const
|
|
23
|
+
|
|
24
|
+
const PLUGIN_SCOPE = "@sap";
|
|
25
|
+
const PLUGIN_PREFIX = `${PLUGIN_SCOPE}/cds`;
|
|
24
26
|
const CUSTOM_RULES_DIR = ".cdslint";
|
|
25
27
|
|
|
26
28
|
const FILES = ["*.cds", "*.csn", "*.csv"];
|
|
@@ -40,13 +42,13 @@ const GLOBALS = {
|
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
module.exports = {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
RULE_FLAVORS,
|
|
46
|
+
DEFAULT_RULE_FLAVOR,
|
|
45
47
|
DEFAULT_RULE_SEVERITY,
|
|
46
48
|
DEFAULT_RULE_TYPE,
|
|
47
49
|
PLUGIN_NAME,
|
|
50
|
+
PLUGIN_SCOPE,
|
|
48
51
|
PLUGIN_PREFIX,
|
|
49
|
-
PROCESSOR_NAME,
|
|
50
52
|
CUSTOM_RULES_DIR,
|
|
51
53
|
FILES,
|
|
52
54
|
MODEL_FILES,
|
package/lib/index.js
CHANGED
|
@@ -1,44 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom ESLint plugin:
|
|
3
3
|
* https://eslint.org/docs/developer-guide/working-with-plugins
|
|
4
|
-
* This file exposes our plugins ESLint configuration, which must:
|
|
4
|
+
* This file exposes our plugins' ESLint configuration, which must:
|
|
5
5
|
* - Expose any 'configs' for prescribed rule configuration bundles
|
|
6
6
|
* (i.e. "recommended"). See shareable configs:
|
|
7
7
|
* https://eslint.org/docs/developer-guide/shareable-configs
|
|
8
|
-
* - Expose any 'globals' for use in ESLint
|
|
9
|
-
* - Expose any 'processors' for use in ESLint
|
|
10
8
|
* - Expose any 'rules' for use in ESLint
|
|
11
9
|
*/
|
|
12
10
|
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
function _getConfig(configName) {
|
|
22
|
-
return {
|
|
23
|
-
globals: GLOBALS,
|
|
24
|
-
plugins: [PLUGIN_NAME],
|
|
25
|
-
overrides: [
|
|
26
|
-
{
|
|
27
|
-
files: FILES,
|
|
28
|
-
processor: PROCESSOR_NAME,
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
rules: rules[configName],
|
|
32
|
-
};
|
|
33
|
-
}
|
|
11
|
+
const api = require("./api");
|
|
12
|
+
const rules = Object.assign(
|
|
13
|
+
{},
|
|
14
|
+
...Object.entries(require("./rules")).map(([k, v]) => ({
|
|
15
|
+
[k]: v() }))
|
|
16
|
+
);
|
|
17
|
+
const configs = require("./conf");
|
|
34
18
|
|
|
35
19
|
module.exports = {
|
|
36
|
-
configs
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
},
|
|
40
|
-
processors: {
|
|
41
|
-
cds: processor,
|
|
42
|
-
},
|
|
43
|
-
rules: rules.sources,
|
|
20
|
+
configs,
|
|
21
|
+
rules,
|
|
22
|
+
...api
|
|
44
23
|
};
|
package/lib/parser.js
CHANGED
|
@@ -6,23 +6,174 @@
|
|
|
6
6
|
* optional properties services, a scopeManager, and visitorKeys
|
|
7
7
|
* - Expose default method 'parse' which should return the AST
|
|
8
8
|
* Both methods should take in the source code and an optional configuration
|
|
9
|
-
* (parserOptions).
|
|
10
|
-
* parser is only used by ESLint's ruleTester.
|
|
9
|
+
* (parserOptions).
|
|
11
10
|
*/
|
|
12
|
-
|
|
11
|
+
const cds = require("@sap/cds");
|
|
12
|
+
const Cache = require("./utils/Cache");
|
|
13
|
+
const LOG = cds.debug("lint:plugin");
|
|
14
|
+
const colors = require("./utils/Colors");
|
|
13
15
|
|
|
14
16
|
module.exports = {
|
|
15
17
|
parse: function (code, options) {
|
|
16
18
|
return module.exports.parseForESLint(code, options).ast;
|
|
17
19
|
},
|
|
18
|
-
|
|
19
20
|
parseForESLint: function (code) {
|
|
20
21
|
return {
|
|
21
|
-
ast:
|
|
22
|
-
services: {
|
|
22
|
+
ast: createProgramAST(code),
|
|
23
|
+
services: {
|
|
24
|
+
getParsedCsn: function () {
|
|
25
|
+
let compiledModel;
|
|
26
|
+
let reflectedModel;
|
|
27
|
+
const messages = [];
|
|
28
|
+
try {
|
|
29
|
+
compiledModel = cds.parse(code);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
// Do nothing
|
|
32
|
+
}
|
|
33
|
+
if (compiledModel) {
|
|
34
|
+
try {
|
|
35
|
+
reflectedModel = cds.linked(compiledModel);
|
|
36
|
+
if (messages) {
|
|
37
|
+
reflectedModel.messages = messages;
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
LOG && LOG(colors.red + 'ERROR:' + colors.reset, err)
|
|
41
|
+
LOG && LOG('COMPILED', compiledModel)
|
|
42
|
+
LOG && LOG('REFLECTED', reflectedModel)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return reflectedModel;
|
|
46
|
+
},
|
|
47
|
+
getInferredCsn: function() {
|
|
48
|
+
const rootPath = Cache.get(`rootpath`);
|
|
49
|
+
if (Cache.has("test")) {
|
|
50
|
+
return Cache.get(`model:${rootPath}`);
|
|
51
|
+
}
|
|
52
|
+
let compiledModel;
|
|
53
|
+
let reflectedModel;
|
|
54
|
+
cds.resolve.cache = {};
|
|
55
|
+
|
|
56
|
+
if (!Cache.has(`model:${rootPath}`)) {
|
|
57
|
+
const roots = cds.resolve("*", { root: rootPath });
|
|
58
|
+
const messages = [];
|
|
59
|
+
if (roots) {
|
|
60
|
+
try {
|
|
61
|
+
compiledModel = cds.load(roots, {
|
|
62
|
+
cwd: ".",
|
|
63
|
+
sync: true,
|
|
64
|
+
locations: true,
|
|
65
|
+
messages,
|
|
66
|
+
});
|
|
67
|
+
Cache.remove('errRootModel');
|
|
68
|
+
} catch (err) {
|
|
69
|
+
Cache.set('errRootModel', err);
|
|
70
|
+
}
|
|
71
|
+
if (compiledModel) {
|
|
72
|
+
reflectedModel = cds.linked(compiledModel);
|
|
73
|
+
Cache.set(`model:${Cache.get(`rootpath`)}`, reflectedModel);
|
|
74
|
+
if (messages) {
|
|
75
|
+
reflectedModel.messages = messages;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
reflectedModel = Cache.get(`model:${rootPath}`);
|
|
81
|
+
}
|
|
82
|
+
return reflectedModel;
|
|
83
|
+
},
|
|
84
|
+
updateInferredCsn: compileModelFromDict,
|
|
85
|
+
getEnvironment: function () {
|
|
86
|
+
const options = Cache.get("options");
|
|
87
|
+
return (options && options[0] && options[0].environment) ? options[0].environment : undefined
|
|
88
|
+
},
|
|
89
|
+
getLocation: function(name, obj, model) {
|
|
90
|
+
let loc;
|
|
91
|
+
const defaultLoc = {
|
|
92
|
+
start: { line: 0, column: 0 },
|
|
93
|
+
end: { line: 1, column: 0 },
|
|
94
|
+
};
|
|
95
|
+
if (obj.$location) {
|
|
96
|
+
const nameloc = obj.$location;
|
|
97
|
+
if (nameloc) {
|
|
98
|
+
// CSN entry with column 0 is equivalent to 'undefined'
|
|
99
|
+
// It means that the column in that line cannot be determined,
|
|
100
|
+
// so we assign a value 1 to get a column location of 0
|
|
101
|
+
if (nameloc.col === 0) {
|
|
102
|
+
nameloc.col = 1;
|
|
103
|
+
}
|
|
104
|
+
loc = defaultLoc;
|
|
105
|
+
loc.start.column = nameloc.col - 1;
|
|
106
|
+
loc.start.line = nameloc.line;
|
|
107
|
+
loc.end.column = nameloc.col - 1 + name.length;
|
|
108
|
+
loc.end.line = nameloc.line;
|
|
109
|
+
} else if (obj.parent) {
|
|
110
|
+
this.getLocation(name, obj.parent, model);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Empty locations default to line 0, column 0
|
|
114
|
+
if (!loc) {
|
|
115
|
+
loc = defaultLoc;
|
|
116
|
+
}
|
|
117
|
+
return loc;
|
|
118
|
+
},
|
|
119
|
+
getNode: function(obj) {
|
|
120
|
+
let loc;
|
|
121
|
+
if (obj && obj.name) {
|
|
122
|
+
loc = this.getLocation(obj.name, obj);
|
|
123
|
+
}
|
|
124
|
+
return createProgramAST(code, loc)
|
|
125
|
+
}
|
|
126
|
+
},
|
|
23
127
|
scopeManager: null,
|
|
24
128
|
tokensAndComments: [],
|
|
25
|
-
visitorKeys:
|
|
129
|
+
visitorKeys: [],
|
|
26
130
|
};
|
|
27
131
|
},
|
|
132
|
+
createProgramAST,
|
|
133
|
+
compileModelFromDict,
|
|
28
134
|
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Generates dummy AST with just single Program node
|
|
138
|
+
* @param code Parse file contents
|
|
139
|
+
* @returns AST
|
|
140
|
+
*/
|
|
141
|
+
function createProgramAST(code, loc) {
|
|
142
|
+
loc = loc || {
|
|
143
|
+
start: {
|
|
144
|
+
line: 1,
|
|
145
|
+
column: 0,
|
|
146
|
+
},
|
|
147
|
+
end: {
|
|
148
|
+
line: 1,
|
|
149
|
+
column: 0,
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
type: "Program",
|
|
154
|
+
body: [],
|
|
155
|
+
sourceType: "module",
|
|
156
|
+
tokens: [],
|
|
157
|
+
comments: [],
|
|
158
|
+
range: [0, code.length],
|
|
159
|
+
loc
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function compileModelFromDict(dictFiles, options) {
|
|
164
|
+
let reflectedModel;
|
|
165
|
+
const messages = [];
|
|
166
|
+
const compiledModel = cds.compile(dictFiles, {
|
|
167
|
+
sync: true,
|
|
168
|
+
locations: true,
|
|
169
|
+
messages,
|
|
170
|
+
...options,
|
|
171
|
+
});
|
|
172
|
+
if (compiledModel) {
|
|
173
|
+
reflectedModel = cds.linked(compiledModel);
|
|
174
|
+
if (messages) {
|
|
175
|
+
reflectedModel.messages = messages;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return reflectedModel;
|
|
179
|
+
}
|
|
@@ -1,36 +1,35 @@
|
|
|
1
|
+
const cds = require("@sap/cds");
|
|
2
|
+
|
|
3
|
+
/** @type {import('../types').Rule} */
|
|
4
|
+
|
|
1
5
|
module.exports = {
|
|
2
6
|
meta: {
|
|
3
7
|
docs: {
|
|
4
8
|
description:
|
|
5
9
|
"Ambiguous key with a `TO MANY` relationship since entries could appear multiple times with the same key.",
|
|
6
10
|
category: "Model Validation",
|
|
7
|
-
recommended: true
|
|
8
|
-
version: "1.0.1",
|
|
11
|
+
recommended: true
|
|
9
12
|
},
|
|
10
|
-
severity: "warn",
|
|
11
13
|
type: "problem",
|
|
14
|
+
model: "inferred"
|
|
12
15
|
},
|
|
13
16
|
create(context) {
|
|
14
|
-
return
|
|
17
|
+
return check_assoc2many_ambiguous_key;
|
|
15
18
|
|
|
16
19
|
function check_assoc2many_ambiguous_key() {
|
|
17
|
-
let reports = [];
|
|
18
20
|
let csnOdata;
|
|
19
|
-
const m = context.
|
|
21
|
+
const m = context.getModel();
|
|
22
|
+
if (!m) return;
|
|
20
23
|
if (m && m.definitions) {
|
|
21
|
-
csnOdata =
|
|
22
|
-
const csnOdataLinked =
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
if (reports.length > 0) {
|
|
26
|
-
return reports;
|
|
24
|
+
csnOdata = cds.compile.for.odata(m);
|
|
25
|
+
const csnOdataLinked = cds.linked(csnOdata);
|
|
26
|
+
associationCardinalityFlaw(csnOdataLinked, context);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
}
|
|
29
|
+
}
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
function associationCardinalityFlaw(csn, context) {
|
|
33
|
-
const reports = [];
|
|
34
33
|
processEntity(csn, (definition, sourceEntity, sourceAlias) => {
|
|
35
34
|
let refCardinalityMult = false;
|
|
36
35
|
let refPlainElement = false;
|
|
@@ -62,33 +61,17 @@ function associationCardinalityFlaw(csn, context) {
|
|
|
62
61
|
) {
|
|
63
62
|
const keyName = Object.keys(definition.keys)[0];
|
|
64
63
|
const key = definition.keys[keyName];
|
|
65
|
-
const keyLoc = context.
|
|
64
|
+
const keyLoc = context.getLocation(keyName, key, csn);
|
|
66
65
|
const colName = column.as ? column.as : column.name;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
};
|
|
73
|
-
const message = `Ambiguous key in '${definition.name}'. Element '${colName}' leads to multiple entries so that key '${keyName}' is not unique.`;
|
|
74
|
-
reports.push(
|
|
75
|
-
{
|
|
76
|
-
message,
|
|
77
|
-
loc: keyLoc,
|
|
78
|
-
file: key.$location.file,
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
message,
|
|
82
|
-
loc: colLoc,
|
|
83
|
-
file: column.$location.file,
|
|
84
|
-
}
|
|
85
|
-
);
|
|
86
|
-
}
|
|
66
|
+
context.report({
|
|
67
|
+
message: `Ambiguous key in '${definition.name}'. Element '${colName}' leads to multiple entries so that key '${keyName}' is not unique.`,
|
|
68
|
+
loc: keyLoc,
|
|
69
|
+
file: key.$location.file
|
|
70
|
+
});
|
|
87
71
|
}
|
|
88
72
|
}
|
|
89
73
|
);
|
|
90
74
|
});
|
|
91
|
-
return reports;
|
|
92
75
|
}
|
|
93
76
|
|
|
94
77
|
function processEntity(csn, eachCallback) {
|
|
@@ -110,7 +93,7 @@ function processEntity(csn, eachCallback) {
|
|
|
110
93
|
sourceEntity = csn.definitions[definition.query.SELECT.from.ref.join("_")];
|
|
111
94
|
sourceAlias.push({
|
|
112
95
|
from: sourceEntity.name,
|
|
113
|
-
as: definition.query.SELECT.from.as || definition.query.SELECT.from.ref.slice(-1)[0].split(".").pop()
|
|
96
|
+
as: definition.query.SELECT.from.as || definition.query.SELECT.from.ref.slice(-1)[0].split(".").pop()
|
|
114
97
|
});
|
|
115
98
|
} else if (definition.query.SELECT.from.args && definition.query.SELECT.from.args[0].ref) {
|
|
116
99
|
// Join
|
|
@@ -118,7 +101,7 @@ function processEntity(csn, eachCallback) {
|
|
|
118
101
|
definition.query.SELECT.from.args.forEach((arg) => {
|
|
119
102
|
sourceAlias.push({
|
|
120
103
|
from: arg.ref.join("_"),
|
|
121
|
-
as: arg.as || arg.ref.slice(-1)[0].split(".").pop()
|
|
104
|
+
as: arg.as || arg.ref.slice(-1)[0].split(".").pop()
|
|
122
105
|
});
|
|
123
106
|
});
|
|
124
107
|
}
|
|
@@ -1,45 +1,37 @@
|
|
|
1
|
-
const { splitEntityName } = require("../utils/ruleHelpers");
|
|
2
|
-
|
|
3
|
-
const SEVERITY = "warn";
|
|
4
1
|
const LABELS = ["@restrict", "@requires"];
|
|
5
2
|
|
|
6
3
|
module.exports = {
|
|
7
4
|
meta: {
|
|
8
5
|
docs: {
|
|
9
|
-
description: "`@restrict` and `@requires` must not be empty",
|
|
6
|
+
description: "`@restrict` and `@requires` must not be empty.",
|
|
10
7
|
category: "Model Validation",
|
|
11
|
-
recommended: true
|
|
12
|
-
version: "2.4.1",
|
|
8
|
+
recommended: true
|
|
13
9
|
},
|
|
14
|
-
severity: SEVERITY,
|
|
15
10
|
hasSuggestions: true,
|
|
16
11
|
messages: {
|
|
17
12
|
InvalidItem: `Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?`,
|
|
18
|
-
ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'
|
|
13
|
+
ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'`
|
|
19
14
|
},
|
|
15
|
+
type: "problem",
|
|
16
|
+
model: "inferred"
|
|
20
17
|
},
|
|
21
18
|
create(context) {
|
|
22
19
|
return {
|
|
23
20
|
entity: check_restrictions,
|
|
24
|
-
service: check_restrictions
|
|
21
|
+
service: check_restrictions
|
|
25
22
|
};
|
|
26
23
|
|
|
27
24
|
function check_restrictions(e) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
LABELS.forEach((l) => {
|
|
31
|
-
const invalid = (e[l] && typeof e[l] === "object" && e[l].length === 0) || (typeof e[l] === "string" && !e[l]);
|
|
25
|
+
for (const l of LABELS) {
|
|
26
|
+
const invalid = (typeof e[l] === "object" && e[l].length === 0) || (typeof e[l] === "string" && e[l] === "");
|
|
32
27
|
if (invalid) {
|
|
33
|
-
|
|
34
|
-
const loc = context.cds.getLocation(entityName, e);
|
|
35
|
-
reports.push({
|
|
28
|
+
context.report({
|
|
36
29
|
message: `No explicit restrictions provided on ${e.kind} \`${e.name}\` at \`${l}\`.`,
|
|
37
|
-
|
|
30
|
+
node: context.getNode(e),
|
|
31
|
+
file: e.$location.file
|
|
38
32
|
});
|
|
39
33
|
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return reports.length > 0 ? reports : undefined;
|
|
34
|
+
}
|
|
43
35
|
}
|
|
44
|
-
}
|
|
36
|
+
}
|
|
45
37
|
};
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
const SEVERITY = "warn";
|
|
2
|
-
|
|
3
1
|
module.exports = {
|
|
4
2
|
meta: {
|
|
5
3
|
docs: {
|
|
6
|
-
description: "Use `@requires` instead of `@restrict.to` in actions and services with unrestricted events",
|
|
4
|
+
description: "Use `@requires` instead of `@restrict.to` in actions and services with unrestricted events.",
|
|
7
5
|
category: "Model Validation",
|
|
8
6
|
recommended: true,
|
|
9
|
-
version: "2.4.1"
|
|
7
|
+
version: "2.4.1"
|
|
10
8
|
},
|
|
11
|
-
severity: SEVERITY,
|
|
12
9
|
hasSuggestions: true,
|
|
13
10
|
messages: {
|
|
14
11
|
InvalidItem: `Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?`,
|
|
15
|
-
ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'
|
|
12
|
+
ReplaceItemWith: `Replace '{{invalid}}' with '{{candidates}}'`
|
|
16
13
|
},
|
|
14
|
+
type: "problem",
|
|
15
|
+
model: "inferred"
|
|
17
16
|
},
|
|
18
|
-
create() {
|
|
17
|
+
create(context) {
|
|
19
18
|
return {
|
|
20
19
|
service: check_restrict,
|
|
21
|
-
action: check_restrict
|
|
20
|
+
action: check_restrict
|
|
22
21
|
};
|
|
23
22
|
|
|
24
23
|
function check_restrict(e) {
|
|
25
|
-
const reports = [];
|
|
26
|
-
|
|
27
24
|
if (e && e["@restrict"] && typeof e["@restrict"] === "object") {
|
|
28
|
-
e["@restrict"]
|
|
25
|
+
for (const entry of e["@restrict"]) {
|
|
29
26
|
const keys = Object.keys(entry);
|
|
30
27
|
if (keys.includes("to") && keys.includes("grant") && entry.grant === "*") {
|
|
31
|
-
|
|
28
|
+
context.report({
|
|
29
|
+
message: `Use \`@requires\` instead of \`@restrict.to\` at ${e.kind} \`${e.name}\`.`,
|
|
30
|
+
node: context.getNode(e),
|
|
31
|
+
file: e.$location.file
|
|
32
|
+
});
|
|
32
33
|
}
|
|
33
|
-
}
|
|
34
|
+
}
|
|
34
35
|
}
|
|
35
|
-
return reports.length > 0 ? reports : undefined;
|
|
36
36
|
}
|
|
37
|
-
}
|
|
37
|
+
}
|
|
38
38
|
};
|