@sap/eslint-plugin-cds 2.4.1 → 2.6.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +31 -2
  2. package/README.md +2 -1
  3. package/lib/api/index.js +13 -12
  4. package/lib/conf/all.js +22 -0
  5. package/lib/conf/index.js +22 -0
  6. package/lib/conf/recommended.js +19 -0
  7. package/lib/constants.js +19 -16
  8. package/lib/index.js +11 -33
  9. package/lib/parser.js +169 -10
  10. package/lib/rules/assoc2many-ambiguous-key.js +80 -96
  11. package/lib/rules/auth-no-empty-restrictions.js +23 -30
  12. package/lib/rules/auth-use-requires.js +25 -24
  13. package/lib/rules/auth-valid-restrict-grant.js +87 -49
  14. package/lib/rules/auth-valid-restrict-keys.js +30 -23
  15. package/lib/rules/auth-valid-restrict-to.js +97 -52
  16. package/lib/rules/auth-valid-restrict-where.js +52 -42
  17. package/lib/rules/extension-restrictions.js +69 -0
  18. package/lib/rules/index.js +27 -0
  19. package/lib/rules/latest-cds-version.js +23 -21
  20. package/lib/rules/min-node-version.js +25 -24
  21. package/lib/rules/no-db-keywords.js +23 -31
  22. package/lib/rules/no-dollar-prefixed-names.js +17 -14
  23. package/lib/rules/no-join-on-draft.js +27 -0
  24. package/lib/rules/require-2many-oncond.js +11 -16
  25. package/lib/rules/sql-cast-suggestion.js +16 -29
  26. package/lib/rules/start-elements-lowercase.js +42 -44
  27. package/lib/rules/start-entities-uppercase.js +29 -31
  28. package/lib/rules/valid-csv-header.js +65 -64
  29. package/lib/{api/lint.d.ts → types.d.ts} +5 -7
  30. package/lib/utils/Cache.js +33 -0
  31. package/lib/utils/Colors.js +9 -0
  32. package/lib/utils/createRule.js +317 -0
  33. package/lib/utils/findFuzzy.js +87 -0
  34. package/lib/utils/genDocs.js +345 -0
  35. package/lib/utils/getConfigPath.js +33 -0
  36. package/lib/utils/getConfiguredFileTypes.js +10 -0
  37. package/lib/utils/getFileExtensions.js +8 -0
  38. package/lib/utils/getProjectRootPath.js +25 -0
  39. package/lib/utils/isConfiguredFileType.js +20 -0
  40. package/lib/utils/rules.js +112 -1041
  41. package/lib/utils/runRuleTester.js +116 -0
  42. package/package.json +10 -4
  43. package/lib/processor.js +0 -50
  44. package/lib/rules/no-join-on-draft-enabled-entities.js +0 -40
  45. package/lib/utils/fuzzySearch.js +0 -94
  46. package/lib/utils/helpers.js +0 -94
  47. package/lib/utils/jsonc.js +0 -1
  48. package/lib/utils/model.js +0 -393
  49. package/lib/utils/ruleHelpers.js +0 -199
  50. package/lib/utils/ruleTester.js +0 -78
  51. package/lib/utils/validate.js +0 -36
package/CHANGELOG.md CHANGED
@@ -6,14 +6,43 @@ 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.6.0] - 2022-09-29
10
+
11
+ ### Changed
12
+
13
+ - Renamed rule `no-join-on-draft-enabled-entities` to `no-join-on-draft`.
14
+ - Expanded list of reserved keywords to check for in rule `no-db-keywords`.
15
+
16
+ ### Added
17
+
18
+ - New `extension-restrictions` rule that validates extension projects' models against restrictions set by the extended SaaS app.
19
+
20
+ ### Fixed
21
+
22
+ - Errors from rules are shown again in the console output
23
+
24
+ ## [2.5.0] - 2022-08-04
25
+
26
+ ### Changed
27
+
28
+ - Model Validation rules use `parsed` flavor by default (`meta.model` property)
29
+ - Environment rules have { model: "none" }
30
+
31
+ ### Added
32
+
33
+ - Flavor in `model` property on `meta` object of rule
34
+ - Context function `getNode()` returns Node with proper location
35
+
9
36
  ## [2.4.1] - 2022-06-17
10
37
 
11
- - Added authorization rules 'auth-*'.
38
+ ### Added
39
+
40
+ - Authorization rules 'auth-*'.
12
41
 
13
42
  ### Changed
14
43
 
15
44
  - Node.js 14 is now the minimum required Node.js version. Version 12 is no longer supported.
16
-
45
+ - Default CSN flavor in rules is `parsed`.
17
46
 
18
47
  ## [2.4.0] - 2022-04-14
19
48
 
package/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # @sap/eslint-plugin-cds
2
-
2
+ [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
3
+ [![Sync Files Workflow](https://github.tools.sap/cap/eslint-plugin-cds/actions/workflows/sync.yml/badge.svg)](https://github.tools.sap/cap/eslint-plugin-cds/actions/workflows/sync.yml)
3
4
 
4
5
  The [ESLint](https://eslint.org) plugin includes a set of recommended [SAP Cloud Application Programming Model (CAP)](https://cap.cloud.sap) model and environment rules. The aim of CDS linting is to catch issues with CDS models and with the environment early. To use this module we recommend to install [@sap/cds-dk](https://www.npmjs.com/package/@sap/cds-dk) globally.
5
6
 
package/lib/api/index.js CHANGED
@@ -1,25 +1,26 @@
1
- "use strict";
1
+ 'use strict'
2
2
 
3
3
  /**
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 'getConfigPath', 'getFileExtensions', and 'genDocs' for usage in
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 { runRuleTester } = require("../utils/ruleTester")
13
- const { getConfigPath } = require("../utils/model");
14
- const { getFileExtensions } = require("../utils/helpers");
15
- const { createRule, genDocs } = require("../utils/rules");
16
- const parserPath = require.resolve("../parser");
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')
17
+ const parserPath = require.resolve('../parser')
17
18
 
18
19
  module.exports = {
19
- createRule,
20
20
  runRuleTester,
21
- getConfigPath,
22
- getFileExtensions,
21
+ createRule,
23
22
  genDocs,
24
- parserPath,
25
- };
23
+ getConfigPath,
24
+ getFileExtensions: getConfiguredFileTypes,
25
+ parserPath
26
+ }
@@ -0,0 +1,22 @@
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': '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
+ '@sap/cds/extension-restrictions': 'error'
22
+ }
@@ -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,19 @@
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': 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': 1,
15
+ '@sap/cds/require-2many-oncond': 2,
16
+ '@sap/cds/sql-cast-suggestion': 1,
17
+ '@sap/cds/valid-csv-header': 1,
18
+ '@sap/cds/extension-restrictions': 2
19
+ }
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,18 +12,21 @@
13
12
  * - GLOBALS: Globals which should be exposed to ESLint by this plugin
14
13
  */
15
14
 
16
- const RULE_CATEGORIES = ["Model Validation", "Environment"];
17
- const DEFAULT_RULE_CATEGORY = RULE_CATEGORIES[0];
18
- const DEFAULT_RULE_SEVERITY = "error";
19
- const DEFAULT_RULE_TYPE = "problem";
15
+ const RULE_FLAVORS = ['parsed', 'inferred']
20
16
 
21
- const PLUGIN_NAME = require("../package.json").name;
22
- const PLUGIN_PREFIX = "@sap/cds";
23
- const PROCESSOR_NAME = `${PLUGIN_PREFIX}/cds`
24
- const CUSTOM_RULES_DIR = ".cdslint";
17
+ const DEFAULT_RULE_CATEGORY = 'Model Validation'
18
+ const DEFAULT_RULE_FLAVOR = RULE_FLAVORS[0]
19
+ const DEFAULT_RULE_SEVERITY = 'error'
20
+ const DEFAULT_RULE_TYPE = 'problem'
25
21
 
26
- const FILES = ["*.cds", "*.csn", "*.csv"];
27
- const MODEL_FILES = ["*.cds", "*.csn"];
22
+ const PLUGIN_NAME = require('../package.json').name
23
+
24
+ const PLUGIN_SCOPE = '@sap'
25
+ const PLUGIN_PREFIX = `${PLUGIN_SCOPE}/cds`
26
+ const CUSTOM_RULES_DIR = '.cdslint'
27
+
28
+ const FILES = ['*.cds', '*.csn', '*.csv']
29
+ const MODEL_FILES = ['*.cds', '*.csn']
28
30
 
29
31
  const GLOBALS = {
30
32
  SELECT: true,
@@ -36,19 +38,20 @@ const GLOBALS = {
36
38
  CDL: true,
37
39
  CQL: true,
38
40
  CXL: true,
39
- cds: true,
40
- };
41
+ cds: true
42
+ }
41
43
 
42
44
  module.exports = {
43
- RULE_CATEGORIES,
45
+ RULE_FLAVORS,
44
46
  DEFAULT_RULE_CATEGORY,
47
+ DEFAULT_RULE_FLAVOR,
45
48
  DEFAULT_RULE_SEVERITY,
46
49
  DEFAULT_RULE_TYPE,
47
50
  PLUGIN_NAME,
51
+ PLUGIN_SCOPE,
48
52
  PLUGIN_PREFIX,
49
- PROCESSOR_NAME,
50
53
  CUSTOM_RULES_DIR,
51
54
  FILES,
52
55
  MODEL_FILES,
53
56
  GLOBALS
54
- };
57
+ }
package/lib/index.js CHANGED
@@ -1,44 +1,22 @@
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 path = require("path");
14
- const processor = require("./processor");
15
-
16
- const { FILES, GLOBALS, PLUGIN_NAME, PROCESSOR_NAME } = require("./constants");
17
- const { getRules } = require("./utils/rules");
18
-
19
- const rules = getRules(path.join(__dirname, "rules"));
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]) => ({ [k]: v() }))
15
+ )
16
+ const configs = require('./conf')
34
17
 
35
18
  module.exports = {
36
- configs: {
37
- recommended: _getConfig("recommended"),
38
- all: _getConfig("all"),
39
- },
40
- processors: {
41
- cds: processor,
42
- },
43
- rules: rules.sources,
44
- };
19
+ configs,
20
+ rules,
21
+ ...api
22
+ }
package/lib/parser.js CHANGED
@@ -6,23 +6,182 @@
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). Note, that because we use a 'empty' preprocessor, the
10
- * parser is only used by ESLint's ruleTester.
9
+ * (parserOptions).
11
10
  */
12
- const { getAST } = require("./utils/model");
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')
15
+ const { splitDefName } = require('./utils/rules')
13
16
 
14
17
  module.exports = {
15
18
  parse: function (code, options) {
16
- return module.exports.parseForESLint(code, options).ast;
19
+ return module.exports.parseForESLint(code, options).ast
17
20
  },
18
-
19
21
  parseForESLint: function (code) {
20
22
  return {
21
- ast: getAST(code),
22
- services: {},
23
+ ast: createProgramAST(code),
24
+ services: {
25
+ getParsedCsn: function () {
26
+ let compiledModel
27
+ let reflectedModel
28
+ const messages = []
29
+ try {
30
+ compiledModel = cds.parse(code)
31
+ } catch (err) {
32
+ // Do nothing
33
+ }
34
+ if (compiledModel) {
35
+ try {
36
+ reflectedModel = cds.linked(compiledModel)
37
+ if (messages) {
38
+ reflectedModel.messages = messages
39
+ }
40
+ } catch (err) {
41
+ LOG && LOG(colors.red + 'ERROR:' + colors.reset, err)
42
+ LOG && LOG('COMPILED', compiledModel)
43
+ LOG && LOG('REFLECTED', reflectedModel)
44
+ }
45
+ }
46
+ return reflectedModel
47
+ },
48
+ getInferredCsn: function () {
49
+ const rootPath = Cache.get('rootpath')
50
+ if (Cache.has('test')) {
51
+ return Cache.get(`model:${rootPath}`)
52
+ }
53
+ let compiledModel
54
+ let reflectedModel
55
+ cds.resolve.cache = {}
56
+
57
+ if (!Cache.has(`model:${rootPath}`) && rootPath) {
58
+ const roots = Cache.get(`roots:${rootPath}`)
59
+ const messages = []
60
+ if (roots) {
61
+ try {
62
+ compiledModel = cds.load(roots, {
63
+ cwd: rootPath,
64
+ sync: true,
65
+ locations: true,
66
+ messages
67
+ })
68
+ Cache.remove('errRootModel')
69
+ } catch (err) {
70
+ Cache.set('errRootModel', err)
71
+ }
72
+ if (compiledModel) {
73
+ reflectedModel = cds.linked(compiledModel)
74
+ Cache.set(`model:${Cache.get('rootpath')}`, reflectedModel)
75
+ if (messages) {
76
+ reflectedModel.messages = messages
77
+ }
78
+ }
79
+ }
80
+ } else {
81
+ reflectedModel = Cache.get(`model:${rootPath}`)
82
+ }
83
+ return reflectedModel
84
+ },
85
+ updateInferredCsn: compileModelFromDict,
86
+ getEnvironment: function () {
87
+ const options = Cache.get('options')
88
+ return (options && options[0] && options[0].environment) ? options[0].environment : undefined
89
+ },
90
+ getLocation: function (name, obj, model) {
91
+ let loc
92
+ const defaultLoc = {
93
+ start: { line: 0, column: 0 },
94
+ end: { line: 1, column: 0 }
95
+ }
96
+ if (obj.$location) {
97
+ const objLoc = obj.$location
98
+ if (objLoc) {
99
+ // CSN entry with column 0 is equivalent to 'undefined'
100
+ // It means that the column in that line cannot be determined,
101
+ // so we assign a value 1 to get a column location of 0
102
+ if (objLoc.col === 0) {
103
+ objLoc.col = 1
104
+ }
105
+ loc = defaultLoc
106
+ loc.start.column = objLoc.col - 1
107
+ loc.start.line = objLoc.line
108
+ let colLength = name?.length // use length of `name` property
109
+ // TODO bug in reflect? : `annotate` elements have an unusable index-like `name`, e.g. "1"
110
+ if (obj.annotate) colLength = 0
111
+ loc.end.column = objLoc.col - 1 + colLength
112
+ loc.end.line = objLoc.line
113
+ } else if (obj.parent) {
114
+ this.getLocation(name, obj.parent, model)
115
+ }
116
+ }
117
+ // Empty locations default to line 0, column 0
118
+ if (!loc) {
119
+ loc = defaultLoc
120
+ }
121
+ return loc
122
+ },
123
+ getNode: function (obj) {
124
+ let loc
125
+ if (obj) {
126
+ let name = obj.name
127
+ if (['entity', 'service'].includes(obj.kind)) {
128
+ name = splitDefName(obj).name
129
+ }
130
+ loc = this.getLocation(name, obj)
131
+ }
132
+ return createProgramAST(code, loc)
133
+ }
134
+ },
23
135
  scopeManager: null,
24
136
  tokensAndComments: [],
25
- visitorKeys: null,
26
- };
137
+ visitorKeys: []
138
+ }
27
139
  },
28
- };
140
+ createProgramAST,
141
+ compileModelFromDict
142
+ }
143
+
144
+ /**
145
+ * Generates dummy AST with just single Program node
146
+ * @param code Parse file contents
147
+ * @returns AST
148
+ */
149
+ function createProgramAST (code, loc) {
150
+ loc = loc || {
151
+ start: {
152
+ line: 1,
153
+ column: 0
154
+ },
155
+ end: {
156
+ line: 1,
157
+ column: 0
158
+ }
159
+ }
160
+ return {
161
+ type: 'Program',
162
+ body: [],
163
+ sourceType: 'module',
164
+ tokens: [],
165
+ comments: [],
166
+ range: [0, code.length],
167
+ loc
168
+ }
169
+ }
170
+
171
+ function compileModelFromDict (dictFiles, options) {
172
+ let reflectedModel
173
+ const messages = []
174
+ const compiledModel = cds.compile(dictFiles, {
175
+ sync: true,
176
+ locations: true,
177
+ messages,
178
+ ...options
179
+ })
180
+ if (compiledModel) {
181
+ reflectedModel = cds.linked(compiledModel)
182
+ if (messages) {
183
+ reflectedModel.messages = messages
184
+ }
185
+ }
186
+ return reflectedModel
187
+ }