@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
@@ -0,0 +1,345 @@
1
+ const fs = require('fs')
2
+ const os = require('os')
3
+ const path = require('path')
4
+ const semver = require('semver')
5
+ const cp = require('child_process')
6
+
7
+ const cds = require('@sap/cds')
8
+ const { mkdirp } = cds.utils
9
+
10
+ const Cache = require('./Cache')
11
+ const IS_WIN = os.platform() === 'win32'
12
+
13
+ const { exit } = require('process')
14
+
15
+ const constants = require('../constants')
16
+
17
+ /**
18
+ * Generates custom rules documentation (markdown files)
19
+ * for user according to contents of:
20
+ * - Rule files
21
+ * - Test files (with valid/invalid/fixed examples)
22
+ */
23
+ module.exports = async (projectPath, customRulesDir, registry, prepareRelease = false) => {
24
+ let docsPath, rulePath, testPath, release
25
+
26
+ Cache.set('testerCases', true)
27
+
28
+ if (!projectPath) {
29
+ docsPath = path.join(__dirname, '../../docs')
30
+ rulePath = path.join(__dirname, '../rules')
31
+ testPath = path.join(__dirname, '../../tests/lib/rules')
32
+ release = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()).version
33
+ } else {
34
+ docsPath = path.join(projectPath, `${customRulesDir}/docs`)
35
+ rulePath = path.join(projectPath, `${customRulesDir}/rules`)
36
+ testPath = path.join(projectPath, `${customRulesDir}/tests`)
37
+ await Promise.all(
38
+ [docsPath, rulePath, testPath].filter((path) => !fs.existsSync(path)).map((path) => mkdirp(path))
39
+ )
40
+ }
41
+
42
+ if (registry) {
43
+ // Get rules (internal on artifactory)
44
+ const versionInternal = prepareRelease
45
+ ? JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()).version
46
+ : getPackageVersion(registry)
47
+ if (versionInternal) {
48
+ console.log(`Updating internal rules from v>=${versionInternal}:\n${registry}\n`)
49
+ const rulesInternal = getRules(docsPath, rulePath, testPath, versionInternal)
50
+ genDocFiles(rulesInternal, docsPath)
51
+ }
52
+ // Get rules released (external on npm)
53
+ const npmRegistry = 'https://registry.npmjs.org'
54
+ const versionExternal = prepareRelease
55
+ ? JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()).version
56
+ : getPackageVersion(npmRegistry)
57
+ if (versionExternal) {
58
+ console.log(`Updating external rules from v>=${versionExternal}:\n${npmRegistry}\n`)
59
+ const rulesExternal = getRules(docsPath, rulePath, testPath, versionExternal, release)
60
+ genDocFiles(rulesExternal, docsPath, release)
61
+ }
62
+ } else {
63
+ // Get "custom" rules
64
+ const rules = getRules(docsPath, rulePath, testPath)
65
+ genDocFiles(rules, docsPath)
66
+ }
67
+ console.log('Done!')
68
+ }
69
+
70
+ /**
71
+ * Generates markdown table of all rules with their respective properties based on input rules
72
+ * @param rules array of rules with mandatory name, details properties
73
+ * @param release
74
+ * @returns markdown table of all rules
75
+ */
76
+ function genMdRules (rules, release = false) {
77
+ let mdRulesTable = ''
78
+ if (rules.length > 0) {
79
+ const emojiRecommended = '✔️'
80
+ const emojiFixable = '🔧'
81
+ const emojiSuggestions = '💡'
82
+ const emojiConstruction = '🚧'
83
+ let mdRulesHeader = 'Rules in ESLint are grouped by type to help you understand their purpose. Each rule has emojis denoting:\n\n'
84
+ mdRulesHeader += `${emojiRecommended} if the plugin's "recommended" configuration enables the rule\n\n`
85
+ mdRulesHeader += `${emojiFixable} if problems reported by the rule are automatically fixable (\`--fix\`)\n\n`
86
+ mdRulesHeader += `${emojiSuggestions} if problems reported by the rule are manually fixable (editor)\n\n`
87
+ if (!release) {
88
+ mdRulesHeader += `${emojiConstruction} if rule exists in plugin (main branch) but is not yet released (artifactory)\n\n`
89
+ mdRulesHeader += '| | | | | | | |\n'
90
+ mdRulesHeader += '|:-:|:-:|:-:|:-:|-:|:-|:-|\n'
91
+ } else {
92
+ mdRulesHeader += '| | | | | | | |\n'
93
+ mdRulesHeader += '|:-:|:-:|:-:|:-:|-:|:-|:-|\n'
94
+ }
95
+ let mdRules = ''
96
+ rules.forEach((rule) => {
97
+ if (rule.name && rule.details) {
98
+ const isRecommended = rule.recommended ? emojiRecommended : ''
99
+ const isFixable = rule.fixable ? emojiFixable : ''
100
+ const hasSuggestions = rule.hasSuggestions ? emojiSuggestions : ''
101
+ const underConstruction = rule.construction ? emojiConstruction : ''
102
+ mdRules += release
103
+ ? `| ${isRecommended} | ${isFixable} | ${hasSuggestions} | |   | [${rule.name}](Rules-released.md#${rule.name}) | ${rule.details}|\n`
104
+ : `| ${isRecommended} | ${isFixable} | ${hasSuggestions} | ${underConstruction} |   | [${rule.name}](Rules.md#${rule.name}) | ${rule.details}|\n`
105
+ }
106
+ })
107
+ mdRulesTable = mdRules ? `${mdRulesHeader}${mdRules}\n` : ''
108
+ }
109
+ return mdRulesTable
110
+ }
111
+
112
+ /**
113
+ * Generates markdown documentation files for:
114
+ * - Overview of all rules in form of markdown table (RuleList)
115
+ * - List of all rules details in form of markdown page (Rules)
116
+ * If used internally within the @sap/eslint-plugin-cds, this
117
+ * also generates 'released' files, which only contain information
118
+ * on rules published until the currently released version.
119
+ * @param rules
120
+ * @param docsPath
121
+ * @param release
122
+ */
123
+ function genDocFiles (rules, docsPath, release = false) {
124
+ let suffix = ''
125
+ if (release) {
126
+ suffix = '-released'
127
+ }
128
+ const ruleDocsPath = path.join(docsPath, `Rules${suffix}.md`)
129
+ const ruleListDocsPath = path.join(docsPath, `RuleList${suffix}.md`)
130
+
131
+ if (!fs.existsSync(ruleDocsPath)) {
132
+ fs.writeFileSync(ruleDocsPath, '', 'utf8')
133
+ }
134
+ if (!fs.existsSync(ruleListDocsPath)) {
135
+ fs.writeFileSync(ruleListDocsPath, '', 'utf8')
136
+ }
137
+ const mdRulesCur = fs.readFileSync(ruleDocsPath, 'utf8')
138
+ const mdRuleListCur = fs.readFileSync(ruleListDocsPath, 'utf8')
139
+
140
+ // Get rules table
141
+
142
+ const header = '# @sap/eslint-plugin-cds [latest]\n\n'
143
+ const mdRuleList = genMdRules(rules, release)
144
+
145
+ // Get rule details
146
+ let mdRules = ''
147
+ /* eslint-disable-next-line no-unused-vars */
148
+ rules.forEach(rule => {
149
+ mdRules += `${rule.contents}\n\n${rule.sources}\n\n---\n\n`
150
+ })
151
+
152
+ if (mdRuleListCur !== mdRuleList || mdRulesCur !== mdRules) {
153
+ fs.writeFileSync(ruleDocsPath, header + mdRules, 'utf8')
154
+ fs.writeFileSync(ruleListDocsPath, header + mdRuleList, 'utf8')
155
+ }
156
+ }
157
+
158
+ function getPackageVersion (registry) {
159
+ let result
160
+ try {
161
+ result = cp
162
+ .execSync(`npm show @sap/eslint-plugin-cds --@sap:registry=${registry} --json`, {
163
+ cwd: process.cwd(),
164
+ shell: IS_WIN,
165
+ stdio: 'pipe'
166
+ })
167
+ .toString()
168
+ } catch (err) {
169
+ console.log(`Failed to connect to ${registry} - check your connection and try again.`)
170
+ exit(0)
171
+ }
172
+ const version = JSON.parse(result).version
173
+ if (!version) {
174
+ console.log(`Failed to get latest plugin version from ${registry} - check your connection and try again.`)
175
+ exit(0)
176
+ }
177
+ return version
178
+ }
179
+
180
+ function getRules (docsPath, rulePath, testPath, versionRequired = '0.0.0', release = false) {
181
+ let mdRule, mdRuleSources, mdRuleContents
182
+ const rules = []
183
+ const ruleVersionsPath = path.join(docsPath, '_data', 'rule_versions.json')
184
+ const ruleVersions = require(ruleVersionsPath)
185
+ let fileNumber = 0
186
+ fs.readdirSync(rulePath).filter((file) => {
187
+ if (path.extname(file).toLowerCase() === '.js' && file !== 'index.js') {
188
+ const rule = path.basename(file).replace(path.extname(file), '')
189
+ const ruleTestPath = path.join(testPath, rule, 'rule.test.js')
190
+ fileNumber++
191
+
192
+ // Get rule meta information
193
+ const ruleMeta = require(path.join(rulePath, file)).meta
194
+ let version = ruleVersions.added[rule]
195
+ if (!version) {
196
+ version = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()).version
197
+ ruleVersions.added[rule] = version
198
+ fs.writeFileSync(ruleVersionsPath, JSON.stringify(ruleVersions, null, 4), 'utf8')
199
+ }
200
+ if ((release && semver.satisfies(version, `<=${versionRequired}`)) || !release) {
201
+ console.log(`${fileNumber}> preparing docs for ${ruleTestPath}`)
202
+
203
+ const details = ruleMeta.docs.description
204
+ const flavor = ruleMeta.model ? ruleMeta.model : constants.DEFAULT_RULE_FLAVOR
205
+ const category = (flavor === 'none') ? 'Environment' : 'Model Validation'
206
+ const fixable = ruleMeta.fixable
207
+ const messages = ruleMeta.messages ? ruleMeta.messages : []
208
+ const recommended = ruleMeta.docs.recommended
209
+ const suggestions = ruleMeta.hasSuggestions
210
+
211
+ let underConstruction = ''
212
+ if (!release && (version === 'TBD' || semver.satisfies(version, `>${versionRequired}`))) {
213
+ underConstruction = '🚧'
214
+ console.log(` > 🚧 Rule '${rule}' still under construction.\n`)
215
+ }
216
+
217
+ const isFixable = ['code', 'whitespace'].includes(fixable) ? '🔧' : ''
218
+ const isRecommended = recommended === true ? '✔️' : ''
219
+ const hasSuggestions = suggestions === true ? '💡' : ''
220
+
221
+ const rulesEntry = {
222
+ name: rule,
223
+ details,
224
+ recommended: isRecommended,
225
+ fixable: isFixable,
226
+ hasSuggestions,
227
+ construction: underConstruction,
228
+ messages,
229
+ version
230
+ }
231
+ try {
232
+ mdRule = getRuleExamples(rule, ruleTestPath, testPath, rulesEntry)
233
+ } catch (err) {
234
+ // Just continue
235
+ }
236
+ mdRuleContents = ''
237
+
238
+ mdRuleContents +=
239
+ !release && underConstruction
240
+ ? `## ${rule}\n<span class='shifted'>${underConstruction}&nbsp;&nbsp;<span class='label'>${category}</span></span>\n\n`
241
+ : `## ${rule}\n<span class='shifted label'>${category}</span>\n\n`
242
+
243
+ mdRuleContents += `### Rule Details\n${details}\n\n`
244
+ if (mdRule) {
245
+ mdRuleContents += `### Examples\n${mdRule}\n\n`
246
+ }
247
+ mdRuleContents += `### Version\nThis rule was introduced in \`@sap/eslint-plugin-cds ${version}\`.\n\n`
248
+ mdRuleSources = `### Resources\n[Rule & Documentation source](${path
249
+ .relative(docsPath, path.join(rulePath, `${rule}.js`))
250
+ .replace(/\\/g, '/')})\n\n`
251
+
252
+ rulesEntry.contents = mdRuleContents
253
+ rulesEntry.sources = mdRuleSources
254
+ rules.push(rulesEntry)
255
+ }
256
+ }
257
+ return undefined
258
+ })
259
+ return rules
260
+ }
261
+
262
+ function getRuleExamples (rule, ruleTestPath, testPath, ruleDictEntry) {
263
+ // Get rule valid/invalid tests
264
+ let mdRule = ''
265
+ if (fs.existsSync(ruleTestPath)) {
266
+ require(`${ruleTestPath}`)
267
+ const testerCases = Cache.get(`testerCases:${rule}`)
268
+ const isEnvRule = testerCases.valid[0].filename === '<text>'
269
+ const valid = !isEnvRule ? fs.readFileSync(testerCases.valid[0].filename, 'utf8') : JSON.stringify(testerCases.valid[0].options[0].environment, null, 4)
270
+ const invalid = !isEnvRule ? fs.readFileSync(testerCases.invalid[0].filename, 'utf8') : JSON.stringify(testerCases.invalid[0].options[0].environment, null, 4)
271
+ let validString = ''
272
+ let invalidString = ''
273
+ if (!isEnvRule) {
274
+ const errors = testerCases.invalid[0].errors
275
+ let errorsSorted = []
276
+ errors.forEach((err) => {
277
+ if (errorsSorted.length === 0) {
278
+ errorsSorted = [err]
279
+ } else {
280
+ const errLast = errorsSorted[errorsSorted.length - 1]
281
+ if (err.line > errLast.line) {
282
+ errorsSorted.push(err)
283
+ } else if (err.line < errLast.line) {
284
+ errorsSorted.unshift(err)
285
+ } else {
286
+ if (err.column > errLast.column) {
287
+ errorsSorted.push(err)
288
+ } else if (err.line < errLast.line) {
289
+ errorsSorted.unshift(err)
290
+ } else {
291
+ if (err.messageId) {
292
+ errorsSorted[errorsSorted.length - 1].messageId += '\n' + err.messageId
293
+ }
294
+ if (err.message) {
295
+ errorsSorted[errorsSorted.length - 1].message += '\n' + err.message
296
+ }
297
+ }
298
+ }
299
+ }
300
+ })
301
+ const code = invalid.split('\n')
302
+ errorsSorted.forEach((err, i) => {
303
+ if (err.messageId && ruleDictEntry.messages) {
304
+ let msg = ruleDictEntry.messages[err.messageId]
305
+ let data
306
+ if (errorsSorted[i].suggestions && errorsSorted[i].suggestions[0]) {
307
+ data = errorsSorted[i].suggestions[0].data
308
+ }
309
+ if (data && msg) {
310
+ Object.keys(data).forEach((d) => {
311
+ msg = msg.replace(`{{${d}}}`, data[d])
312
+ })
313
+ }
314
+ err.message = msg
315
+ }
316
+ const msg = err.message && err.message.includes && err.message.includes('"') ? err.message.replace(/"/gm, '`') : err.message
317
+ if (err.line) {
318
+ const stringStart = errorsSorted[i - 1] ? errorsSorted[i - 1].line : 0
319
+ invalidString += code.slice(stringStart, err.line - 1).join('\n')
320
+ const errorString = err.line === err.endLine ? code[err.line - 1] : code.slice(err.line - 1, err.endLine - 1).join('\n')
321
+ const replacedErrorLine = errorString.substring(0, err.column - 1) +
322
+ `<span style="display:inline-block; position:relative; color:red; border-bottom:2pt dotted red" title="${msg}"><b><i>` +
323
+ errorString.substring(err.column - 1, err.endColumn - 1) +
324
+ '</i></b></span>' + errorString.substring(err.endColumn - 1)
325
+ invalidString += '\n' + replacedErrorLine
326
+ const stringEnd = errorsSorted[i + 1] ? code.slice(err.line, errorsSorted[i + 1].line - 1).join('\n') : code.slice(err.line).join('\n')
327
+ invalidString += errorsSorted[i + 1] ? stringEnd : '\n' + stringEnd
328
+ }
329
+ })
330
+ } else {
331
+ invalidString = invalid
332
+ }
333
+ validString = valid
334
+
335
+ mdRule +=
336
+ '<span>✔️&nbsp;&nbsp; Example of ' +
337
+ '<span style="color:green">correct</span> ' +
338
+ `code for this rule:</span>\n\n<pre><code>${validString.trim()}</code></pre>\n\n`
339
+ mdRule +=
340
+ '<span>❌&nbsp;&nbsp; Example of ' +
341
+ '<span style="color:red">incorrect</span> ' +
342
+ `code for this rule:</span>\n\n<pre><code>${invalidString.trim()}</code></pre>`
343
+ }
344
+ return mdRule
345
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Searches for ESLint config file types (in order or precedence)
3
+ * and returns corresponding directory (usually project's root dir)
4
+ * https://eslint.org/docs/user-guide/configuring#configuration-file-formats
5
+ * @param {string} currentDir start here and search until root dir
6
+ * @returns {string} dir containing ESLint config file (empty if not exists)
7
+ */
8
+
9
+ const fs = require('fs')
10
+ const path = require('path')
11
+
12
+ module.exports = (currentDir = '.') => {
13
+ const configFiles = [
14
+ '.eslintrc.js',
15
+ '.eslintrc.cjs',
16
+ '.eslintrc.yaml',
17
+ '.eslintrc.yml',
18
+ '.eslintrc.json',
19
+ '.eslintrc',
20
+ 'package.json'
21
+ ]
22
+ let configDir = path.resolve(currentDir)
23
+ while (configDir !== path.resolve(configDir, '..')) {
24
+ for (const configFile of configFiles) {
25
+ const configPath = path.join(configDir, configFile)
26
+ if (fs.existsSync(configPath) && fs.statSync(configPath).isFile()) {
27
+ return configPath
28
+ }
29
+ }
30
+ configDir = path.join(configDir, '..')
31
+ }
32
+ return ''
33
+ }
@@ -0,0 +1,10 @@
1
+ const { FILES } = require('../constants')
2
+
3
+ /**
4
+ * Returns an array of allowed file extensions
5
+ * the plugin can parse of the form "*.ext"
6
+ * @returns { ConfigOverrideFiles } Array of file extensions
7
+ */
8
+ module.exports = () => {
9
+ return FILES
10
+ }
@@ -0,0 +1,8 @@
1
+ const constants = require('../constants')
2
+
3
+ /**
4
+ * Returns an array of allowed file extensions
5
+ * the plugin can parse of the form "*.ext"
6
+ * @returns { ConfigOverrideFiles } Array of file extensions
7
+ */
8
+ module.exports = () => constants.FILES
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Searches for directory containing cds roots
3
+ * @param {string} currentDir start here and search until root dir
4
+ * @returns {string} dir containing cds roots (empty if not exists)
5
+ */
6
+
7
+ const path = require('path')
8
+ const cds = require('@sap/cds')
9
+ const Cache = require('./Cache')
10
+
11
+ module.exports = (currentDir = '.') => {
12
+ let dir = path.resolve(currentDir)
13
+ while (dir !== path.resolve(dir, '..')) {
14
+ // @ts-ignore
15
+ cds.resolve.cache = {}
16
+ // @ts-ignore
17
+ const roots = cds.resolve('*', { root: dir })
18
+ if (roots && roots.length > 0) {
19
+ Cache.set(`roots:${dir}`, roots)
20
+ return dir
21
+ }
22
+ dir = path.join(dir, '..')
23
+ }
24
+ return ''
25
+ }
@@ -0,0 +1,20 @@
1
+ const { FILES, MODEL_FILES } = require('../constants')
2
+
3
+ /**
4
+ * Checks whether the given filePath matches a regex `files`
5
+ * @param {string} filePath
6
+ * @returns boolean
7
+ */
8
+ module.exports = (filePath, fileType) => {
9
+ const genRegex = (key) => new RegExp(`${key.map((file) => file.replace('*', '')).join('$|')}$`)
10
+ let isValid = false
11
+ switch (fileType) {
12
+ case 'MODEL_FILES':
13
+ isValid = genRegex(MODEL_FILES).test(filePath)
14
+ break
15
+ case 'FILES':
16
+ isValid = genRegex(FILES).test(filePath)
17
+ break
18
+ }
19
+ return isValid
20
+ }