@sap/eslint-plugin-cds 3.0.5 → 3.1.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 +32 -0
- package/README.md +1 -1
- package/lib/api/index.js +4 -4
- package/lib/conf/all.js +17 -17
- package/lib/conf/experimental.js +12 -0
- package/lib/conf/index.js +12 -3
- package/lib/conf/recommended.js +14 -14
- package/lib/constants.js +2 -0
- package/lib/index.js +2 -1
- package/lib/parser.js +10 -1
- package/lib/rules/assoc2many-ambiguous-key.js +40 -11
- package/lib/rules/auth-no-empty-restrictions.js +36 -10
- package/lib/rules/auth-restrict-grant-service.js +19 -20
- package/lib/rules/auth-use-requires.js +25 -15
- package/lib/rules/auth-valid-restrict-grant.js +137 -81
- package/lib/rules/auth-valid-restrict-keys.js +34 -18
- package/lib/rules/auth-valid-restrict-to.js +67 -60
- package/lib/rules/auth-valid-restrict-where.js +31 -44
- package/lib/rules/extension-restrictions.js +11 -3
- package/lib/rules/index.js +5 -1
- package/lib/rules/latest-cds-version.js +5 -4
- package/lib/rules/no-db-keywords.js +14 -5
- package/lib/rules/no-dollar-prefixed-names.js +9 -2
- package/lib/rules/no-java-keywords.js +181 -0
- package/lib/rules/no-join-on-draft.js +9 -3
- package/lib/rules/sql-cast-suggestion.js +19 -15
- package/lib/rules/sql-null-comparison.js +60 -0
- package/lib/rules/start-elements-lowercase.js +6 -2
- package/lib/rules/start-entities-uppercase.js +12 -5
- package/lib/rules/valid-csv-header.js +33 -13
- package/lib/types.d.ts +4 -4
- package/lib/utils/Cache.js +4 -2
- package/lib/utils/Colors.js +2 -0
- package/lib/utils/LintError.js +17 -0
- package/lib/utils/createRule.js +160 -134
- package/lib/utils/csnTraversal.js +163 -0
- package/lib/utils/findFuzzy.js +15 -7
- package/lib/utils/getConfigPath.js +4 -2
- package/lib/utils/getConfiguredFileTypes.js +2 -0
- package/lib/utils/getFileExtensions.js +2 -0
- package/lib/utils/getProjectRootPath.js +53 -15
- package/lib/utils/isConfiguredFileType.js +8 -3
- package/lib/utils/rules.js +13 -7
- package/lib/utils/runRuleTester.js +69 -36
- package/package.json +1 -1
- package/lib/utils/genDocs.js +0 -346
|
@@ -1,25 +1,63 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const cds = require('@sap/cds')
|
|
5
|
+
const Cache = require('./Cache')
|
|
6
|
+
const fs = require('node:fs')
|
|
7
|
+
|
|
8
|
+
const commonCapProjectFiles = ['build.gradle', '.git', 'srv', 'db', 'app']
|
|
9
|
+
|
|
1
10
|
/**
|
|
2
11
|
* Searches for directory containing cds roots
|
|
12
|
+
*
|
|
13
|
+
* As of today, there is no unified way to find the root directory for a CDS project.
|
|
14
|
+
* ("The root is wherever the user typed `cds init`")
|
|
15
|
+
* We are therefore trying to resolve that path heuristically by ascending through the
|
|
16
|
+
* directory structure, looking for certain files.
|
|
17
|
+
*
|
|
18
|
+
* FIXME: Revisit and use a unified way once available, i.e. from @sap/cds
|
|
19
|
+
*
|
|
3
20
|
* @param {string} currentDir start here and search until root dir
|
|
4
21
|
* @returns {string} dir containing cds roots (empty if not exists)
|
|
5
22
|
*/
|
|
6
|
-
|
|
7
|
-
const path = require('path')
|
|
8
|
-
const cds = require('@sap/cds')
|
|
9
|
-
const Cache = require('./Cache')
|
|
10
|
-
|
|
11
|
-
module.exports = (currentDir = '.') => {
|
|
23
|
+
module.exports = function getProjectRootPath(currentDir = '.') {
|
|
12
24
|
let dir = path.resolve(currentDir)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const roots = cds.resolve('*', { root: dir })
|
|
18
|
-
if (roots && roots.length > 0) {
|
|
19
|
-
Cache.set(`roots:${dir}`, roots)
|
|
20
|
-
return dir
|
|
21
|
-
}
|
|
25
|
+
|
|
26
|
+
while (!couldBeProjectRoot(dir)) {
|
|
27
|
+
if (dir === path.resolve(dir, '..'))
|
|
28
|
+
return '' // we reached the file system root -> abort
|
|
22
29
|
dir = path.join(dir, '..')
|
|
23
30
|
}
|
|
31
|
+
|
|
32
|
+
cds.resolve.cache = {}
|
|
33
|
+
const roots = cds.resolve('*', { root: dir })
|
|
34
|
+
if (roots?.length > 0) {
|
|
35
|
+
Cache.set(`roots:${dir}`, roots)
|
|
36
|
+
return dir
|
|
37
|
+
}
|
|
24
38
|
return ''
|
|
25
39
|
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Checks whether the given directory could be a CDS project root.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} dir
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function couldBeProjectRoot(dir) {
|
|
48
|
+
return isRootPackageJson(path.join(dir, 'package.json')) ||
|
|
49
|
+
commonCapProjectFiles.some(entry => fs.existsSync(path.join(dir, entry)))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isRootPackageJson(filepath) {
|
|
53
|
+
const filename = path.basename(filepath)
|
|
54
|
+
if (filename !== 'package.json')
|
|
55
|
+
return false
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const config = JSON.parse(fs.readFileSync(filepath, 'utf8'))
|
|
59
|
+
return Object.keys(config?.dependencies ?? {}).some(dep => dep === '@sap/cds')
|
|
60
|
+
} catch {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const { FILES, MODEL_FILES } = require('../constants')
|
|
2
4
|
|
|
3
5
|
const regexByFileType = {
|
|
@@ -8,13 +10,16 @@ const regexByFileType = {
|
|
|
8
10
|
/**
|
|
9
11
|
* Checks whether the given filePath matches a regex `files`
|
|
10
12
|
* @param {string} filePath
|
|
13
|
+
* @param {string} fileType
|
|
11
14
|
* @returns boolean
|
|
12
15
|
*/
|
|
13
16
|
module.exports = (filePath, fileType) => {
|
|
14
|
-
|
|
15
|
-
return isValid
|
|
17
|
+
return regexByFileType[fileType].test(filePath)
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* @param arr
|
|
22
|
+
*/
|
|
18
23
|
function globArrayToRegex (arr) {
|
|
19
|
-
return new RegExp(`${arr.map(
|
|
24
|
+
return new RegExp(`${arr.map(e => e.replace('*', '')).join('$|')}$`)
|
|
20
25
|
}
|
package/lib/utils/rules.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const SEP = '[,;\t]'
|
|
2
4
|
const EOL = '\\r?\\n'
|
|
3
5
|
|
|
@@ -9,7 +11,7 @@ module.exports = {
|
|
|
9
11
|
*
|
|
10
12
|
* @param {*} e
|
|
11
13
|
*/
|
|
12
|
-
splitDefName
|
|
14
|
+
splitDefName(e) {
|
|
13
15
|
// Entity names from CSN are of the form:
|
|
14
16
|
// <namespace>.<service>.<def>.<'texts'|'localized'>|<composition value>
|
|
15
17
|
let prefix = ''
|
|
@@ -22,7 +24,7 @@ module.exports = {
|
|
|
22
24
|
// Managed composition get compiler tag `_up`
|
|
23
25
|
let isManagedComposition = false
|
|
24
26
|
if (e.elements) {
|
|
25
|
-
isManagedComposition = Object.keys(e.elements).some(
|
|
27
|
+
isManagedComposition = Object.keys(e.elements).some(k => k === 'up_')
|
|
26
28
|
}
|
|
27
29
|
// Check for compiler tags
|
|
28
30
|
const compilerTagsToExclude = ['texts', 'localized']
|
|
@@ -51,11 +53,15 @@ module.exports = {
|
|
|
51
53
|
return code.indexOf(miss)
|
|
52
54
|
},
|
|
53
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Returns true if the given string is non-empty. The string is trimmed, i.e. leading/trailing
|
|
58
|
+
* whitespace is removed prior to checking.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} value
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
54
63
|
isEmptyString: function (value) {
|
|
55
|
-
|
|
56
|
-
return false
|
|
57
|
-
}
|
|
58
|
-
return true
|
|
64
|
+
return value?.trim() === ''
|
|
59
65
|
},
|
|
60
66
|
|
|
61
67
|
isEmptyObject: function (value) {
|
|
@@ -96,7 +102,7 @@ module.exports = {
|
|
|
96
102
|
const suggest = {
|
|
97
103
|
messageId: 'ReplaceItemWith',
|
|
98
104
|
data: { invalid, candidates },
|
|
99
|
-
fix:
|
|
105
|
+
fix: fixer => fixer.replaceTextRange([startIndex, startIndex + invalid.length] + 1, candidates)
|
|
100
106
|
}
|
|
101
107
|
return ({
|
|
102
108
|
messageId: 'InvalidItem',
|
|
@@ -1,11 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/** @typedef {import('eslint').Rule.RuleModule} RuleModule */
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs')
|
|
6
|
+
const path = require('node:path')
|
|
3
7
|
|
|
4
8
|
const { Linter, RuleTester } = require('eslint')
|
|
5
9
|
const Cache = require('./Cache')
|
|
6
|
-
const createRule = require('./createRule')
|
|
7
10
|
const isConfiguredFileType = require('./isConfiguredFileType')
|
|
8
11
|
const { compileModelFromDict } = require('../parser')
|
|
12
|
+
const rules = require('../rules')
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A wrapper around the return value of `createRule()` that initializes the global
|
|
16
|
+
* cache only when the rule is actually executed. This allows tests to be run
|
|
17
|
+
* with test runners that don't set up a new environment for each test, such as
|
|
18
|
+
* mocha or the Node test runner.
|
|
19
|
+
*
|
|
20
|
+
* @param {RuleModule} rule
|
|
21
|
+
* @returns {RuleModule}
|
|
22
|
+
*/
|
|
23
|
+
function testRuleWrapper(rule) {
|
|
24
|
+
return { ...rule, create: prepareAndRunRule }
|
|
25
|
+
function prepareAndRunRule(context) {
|
|
26
|
+
return {
|
|
27
|
+
Program: node => {
|
|
28
|
+
const filePath = context.getFilename()
|
|
29
|
+
_initModelRuleTester(filePath, rule.meta.model)
|
|
30
|
+
const createValue = rule.create(context)
|
|
31
|
+
const result = createValue.Program(node)
|
|
32
|
+
Cache.clear()
|
|
33
|
+
return result
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
9
38
|
|
|
10
39
|
/**
|
|
11
40
|
* ESLint RuleTester (used by custom rule creator api)
|
|
@@ -13,51 +42,50 @@ const { compileModelFromDict } = require('../parser')
|
|
|
13
42
|
* valid/invalid checks:
|
|
14
43
|
* Model checks require input 'code' entries
|
|
15
44
|
* Env checks require input 'options' with selected parameters
|
|
45
|
+
*
|
|
16
46
|
* @param { CDSRuleTestOpts } options RuleTester input options
|
|
17
|
-
* @returns RuleTester results
|
|
18
47
|
*/
|
|
19
|
-
module.exports = (options)
|
|
48
|
+
module.exports = function runRuleTester(options) {
|
|
49
|
+
const pluginRootPath = path.resolve(__dirname, '../..')
|
|
20
50
|
let parserPath
|
|
21
51
|
let rule = {}
|
|
22
|
-
Cache.set('rules', require(path.join(__dirname, '../rules')))
|
|
23
52
|
const rulename = path.basename(options.root)
|
|
24
|
-
if (options.root.startsWith(
|
|
53
|
+
if (options.root.startsWith(pluginRootPath)) {
|
|
25
54
|
// For plugin's internal tests, resolve parser from here
|
|
26
55
|
parserPath = require.resolve('../parser')
|
|
27
|
-
|
|
28
|
-
rule = createRule(require(`../rules/${path.basename(options.root)}`))
|
|
29
|
-
Cache.set('pluginpath', pluginPath)
|
|
56
|
+
rule = testRuleWrapper(rules[path.basename(options.root)]())
|
|
30
57
|
} else {
|
|
31
58
|
// Otherwise from project root
|
|
59
|
+
// eslint-disable-next-line
|
|
32
60
|
const resolvedPlugin = require.resolve('@sap/eslint-plugin-cds', {
|
|
33
61
|
paths: [options.root]
|
|
34
62
|
})
|
|
35
63
|
parserPath = path.join(path.dirname(resolvedPlugin), 'parser')
|
|
36
|
-
rule = require(path.join(options.root, `../../rules/${path.basename(options.root)}`))
|
|
37
|
-
const pluginPath = path.join(path.dirname(options.root), '../../..')
|
|
38
|
-
Cache.set('pluginpath', pluginPath)
|
|
64
|
+
rule = testRuleWrapper(require(path.join(options.root, `../../rules/${path.basename(options.root)}`)))
|
|
39
65
|
}
|
|
40
|
-
|
|
66
|
+
|
|
67
|
+
let tester
|
|
41
68
|
if (parserPath) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} else {
|
|
46
|
-
options = { parser: parserPath }
|
|
47
|
-
}
|
|
69
|
+
const options = _isEslint9OrLater()
|
|
70
|
+
? { languageOptions: { parser: require(parserPath) } }
|
|
71
|
+
: { parser: parserPath }
|
|
48
72
|
tester = new RuleTester(options)
|
|
73
|
+
} else {
|
|
74
|
+
tester = new RuleTester()
|
|
49
75
|
}
|
|
50
76
|
|
|
51
77
|
const testerCases = {};
|
|
52
|
-
['valid', 'invalid'].forEach(
|
|
78
|
+
['valid', 'invalid'].forEach(type => {
|
|
53
79
|
const filePath = path.join(options.root, `${type}/${options.filename}`)
|
|
54
|
-
Cache.set('rootpath', path.dirname(filePath))
|
|
55
|
-
_initModelRuleTester(filePath, rule.meta?.model)
|
|
56
80
|
testerCases[type] = [
|
|
57
81
|
{
|
|
58
|
-
filename: filePath
|
|
82
|
+
filename: filePath,
|
|
59
83
|
}
|
|
60
84
|
]
|
|
85
|
+
if (_isEslint9OrLater()) {
|
|
86
|
+
// property not supported for ESLint 8
|
|
87
|
+
testerCases[type][0].name = `${path.basename(options.root)}/${type}/${options.filename}`
|
|
88
|
+
}
|
|
61
89
|
if (!isConfiguredFileType(options.filename, 'FILES')) {
|
|
62
90
|
const fileContents = JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
63
91
|
testerCases[type][0].code = ''
|
|
@@ -77,22 +105,22 @@ module.exports = (options) => {
|
|
|
77
105
|
}
|
|
78
106
|
}
|
|
79
107
|
})
|
|
80
|
-
if (Cache.get('testerCases')) {
|
|
81
|
-
Cache.set(`testerCases:${rulename}`, testerCases)
|
|
82
|
-
}
|
|
83
108
|
return tester.run(rulename, rule, testerCases)
|
|
84
109
|
}
|
|
85
110
|
|
|
86
111
|
/**
|
|
87
112
|
* Creates a model for ESLint unit tests
|
|
113
|
+
* @param {string} filePath
|
|
114
|
+
* @param {string} flavor
|
|
88
115
|
*/
|
|
89
116
|
function _initModelRuleTester(filePath, flavor) {
|
|
117
|
+
Cache.set('rules', rules)
|
|
90
118
|
Cache.set('test', true)
|
|
91
119
|
const rootPath = path.dirname(filePath)
|
|
92
120
|
Cache.set('rootpath', rootPath)
|
|
93
121
|
if (flavor !== 'none') { // not for env rules
|
|
94
122
|
const files = fs.readdirSync(rootPath)
|
|
95
|
-
const modelfiles = files.map(
|
|
123
|
+
const modelfiles = files.map(f => path.join(rootPath, f)).filter(fp => isConfiguredFileType(fp, 'MODEL_FILES'))
|
|
96
124
|
Cache.set(`modelfiles:${rootPath}`, modelfiles)
|
|
97
125
|
const dictFiles = _getDictFiles(rootPath, modelfiles)
|
|
98
126
|
Cache.set(`dictfiles:${rootPath}`, dictFiles)
|
|
@@ -102,18 +130,19 @@ function _initModelRuleTester(filePath, flavor) {
|
|
|
102
130
|
}
|
|
103
131
|
|
|
104
132
|
/**
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
133
|
+
* Creates or updates a dictionary of files/file contents for a given
|
|
134
|
+
* project path.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} input
|
|
137
|
+
* @param {string[]} filenames
|
|
138
|
+
* @returns {Record<string, string>} dictFiles
|
|
139
|
+
*/
|
|
140
|
+
function _getDictFiles(input, filenames) {
|
|
112
141
|
let dictFiles = {}
|
|
113
142
|
if (Cache.has(`dictfiles:${input}`)) {
|
|
114
143
|
dictFiles = Cache.get(`dictfiles:${input}`)
|
|
115
144
|
} else {
|
|
116
|
-
|
|
145
|
+
filenames.forEach(file => {
|
|
117
146
|
dictFiles[file] = Cache.has(`file:${file}`)
|
|
118
147
|
? Cache.get(`file:${file}`)
|
|
119
148
|
: fs.readFileSync(file, 'utf8')
|
|
@@ -121,3 +150,7 @@ function _getDictFiles(input, files) {
|
|
|
121
150
|
}
|
|
122
151
|
return dictFiles
|
|
123
152
|
}
|
|
153
|
+
|
|
154
|
+
function _isEslint9OrLater() {
|
|
155
|
+
return Number(Linter.version.split('.')[0]) >= 9
|
|
156
|
+
}
|
package/package.json
CHANGED
package/lib/utils/genDocs.js
DELETED
|
@@ -1,346 +0,0 @@
|
|
|
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
|
-
const LOG = process.env.SILENT ? undefined : constants.log
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Generates custom rules documentation (markdown files)
|
|
20
|
-
* for user according to contents of:
|
|
21
|
-
* - Rule files
|
|
22
|
-
* - Test files (with valid/invalid/fixed examples)
|
|
23
|
-
*/
|
|
24
|
-
module.exports = async (projectPath, customRulesDir, registry, prepareRelease = false) => {
|
|
25
|
-
let docsPath, rulePath, testPath, release
|
|
26
|
-
|
|
27
|
-
Cache.set('testerCases', true)
|
|
28
|
-
|
|
29
|
-
if (!projectPath) {
|
|
30
|
-
docsPath = path.join(__dirname, '../../docs')
|
|
31
|
-
rulePath = path.join(__dirname, '../rules')
|
|
32
|
-
testPath = path.join(__dirname, '../../tests/lib/rules')
|
|
33
|
-
release = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()).version
|
|
34
|
-
} else {
|
|
35
|
-
docsPath = path.join(projectPath, `${customRulesDir}/docs`)
|
|
36
|
-
rulePath = path.join(projectPath, `${customRulesDir}/rules`)
|
|
37
|
-
testPath = path.join(projectPath, `${customRulesDir}/tests`)
|
|
38
|
-
await Promise.all(
|
|
39
|
-
[docsPath, rulePath, testPath].filter((path) => !fs.existsSync(path)).map((path) => mkdirp(path))
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (registry) {
|
|
44
|
-
// Get rules (internal on artifactory)
|
|
45
|
-
const versionInternal = prepareRelease
|
|
46
|
-
? JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()).version
|
|
47
|
-
: getPackageVersion(registry)
|
|
48
|
-
if (versionInternal) {
|
|
49
|
-
LOG?.(`Updating internal rules from v>=${versionInternal}:\n${registry}\n`)
|
|
50
|
-
const rulesInternal = getRules(docsPath, rulePath, testPath, versionInternal)
|
|
51
|
-
genDocFiles(rulesInternal, docsPath)
|
|
52
|
-
}
|
|
53
|
-
// Get rules released (external on npm)
|
|
54
|
-
const npmRegistry = 'https://registry.npmjs.org'
|
|
55
|
-
const versionExternal = prepareRelease
|
|
56
|
-
? JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()).version
|
|
57
|
-
: getPackageVersion(npmRegistry)
|
|
58
|
-
if (versionExternal) {
|
|
59
|
-
LOG?.(`Updating external rules from v>=${versionExternal}:\n${npmRegistry}\n`)
|
|
60
|
-
const rulesExternal = getRules(docsPath, rulePath, testPath, versionExternal, release)
|
|
61
|
-
genDocFiles(rulesExternal, docsPath, release)
|
|
62
|
-
}
|
|
63
|
-
} else {
|
|
64
|
-
// Get "custom" rules
|
|
65
|
-
const rules = getRules(docsPath, rulePath, testPath)
|
|
66
|
-
genDocFiles(rules, docsPath)
|
|
67
|
-
}
|
|
68
|
-
LOG?.('Done!')
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Generates markdown table of all rules with their respective properties based on input rules
|
|
73
|
-
* @param rules array of rules with mandatory name, details properties
|
|
74
|
-
* @param release
|
|
75
|
-
* @returns markdown table of all rules
|
|
76
|
-
*/
|
|
77
|
-
function genMdRules (rules, release = false) {
|
|
78
|
-
let mdRulesTable = ''
|
|
79
|
-
if (rules.length > 0) {
|
|
80
|
-
const emojiRecommended = '✔️'
|
|
81
|
-
const emojiFixable = '🔧'
|
|
82
|
-
const emojiSuggestions = '💡'
|
|
83
|
-
const emojiConstruction = '🚧'
|
|
84
|
-
let mdRulesHeader = 'Rules in ESLint are grouped by type to help you understand their purpose. Each rule has emojis denoting:\n\n'
|
|
85
|
-
mdRulesHeader += `${emojiRecommended} if the plugin's "recommended" configuration enables the rule\n\n`
|
|
86
|
-
mdRulesHeader += `${emojiFixable} if problems reported by the rule are automatically fixable (\`--fix\`)\n\n`
|
|
87
|
-
mdRulesHeader += `${emojiSuggestions} if problems reported by the rule are manually fixable (editor)\n\n`
|
|
88
|
-
if (!release) {
|
|
89
|
-
mdRulesHeader += `${emojiConstruction} if rule exists in plugin (main branch) but is not yet released (artifactory)\n\n`
|
|
90
|
-
mdRulesHeader += '| | | | | | | |\n'
|
|
91
|
-
mdRulesHeader += '|:-:|:-:|:-:|:-:|-:|:-|:-|\n'
|
|
92
|
-
} else {
|
|
93
|
-
mdRulesHeader += '| | | | | | | |\n'
|
|
94
|
-
mdRulesHeader += '|:-:|:-:|:-:|:-:|-:|:-|:-|\n'
|
|
95
|
-
}
|
|
96
|
-
let mdRules = ''
|
|
97
|
-
rules.forEach((rule) => {
|
|
98
|
-
if (rule.name && rule.details) {
|
|
99
|
-
const isRecommended = rule.recommended ? emojiRecommended : ''
|
|
100
|
-
const isFixable = rule.fixable ? emojiFixable : ''
|
|
101
|
-
const hasSuggestions = rule.hasSuggestions ? emojiSuggestions : ''
|
|
102
|
-
const underConstruction = rule.construction ? emojiConstruction : ''
|
|
103
|
-
mdRules += release
|
|
104
|
-
? `| ${isRecommended} | ${isFixable} | ${hasSuggestions} | | | [${rule.name}](Rules-released.md#${rule.name}) | ${rule.details}|\n`
|
|
105
|
-
: `| ${isRecommended} | ${isFixable} | ${hasSuggestions} | ${underConstruction} | | [${rule.name}](Rules.md#${rule.name}) | ${rule.details}|\n`
|
|
106
|
-
}
|
|
107
|
-
})
|
|
108
|
-
mdRulesTable = mdRules ? `${mdRulesHeader}${mdRules}\n` : ''
|
|
109
|
-
}
|
|
110
|
-
return mdRulesTable
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Generates markdown documentation files for:
|
|
115
|
-
* - Overview of all rules in form of markdown table (RuleList)
|
|
116
|
-
* - List of all rules details in form of markdown page (Rules)
|
|
117
|
-
* If used internally within the @sap/eslint-plugin-cds, this
|
|
118
|
-
* also generates 'released' files, which only contain information
|
|
119
|
-
* on rules published until the currently released version.
|
|
120
|
-
* @param rules
|
|
121
|
-
* @param docsPath
|
|
122
|
-
* @param release
|
|
123
|
-
*/
|
|
124
|
-
function genDocFiles (rules, docsPath, release = false) {
|
|
125
|
-
let suffix = ''
|
|
126
|
-
if (release) {
|
|
127
|
-
suffix = '-released'
|
|
128
|
-
}
|
|
129
|
-
const ruleDocsPath = path.join(docsPath, `Rules${suffix}.md`)
|
|
130
|
-
const ruleListDocsPath = path.join(docsPath, `RuleList${suffix}.md`)
|
|
131
|
-
|
|
132
|
-
if (!fs.existsSync(ruleDocsPath)) {
|
|
133
|
-
fs.writeFileSync(ruleDocsPath, '', 'utf8')
|
|
134
|
-
}
|
|
135
|
-
if (!fs.existsSync(ruleListDocsPath)) {
|
|
136
|
-
fs.writeFileSync(ruleListDocsPath, '', 'utf8')
|
|
137
|
-
}
|
|
138
|
-
const mdRulesCur = fs.readFileSync(ruleDocsPath, 'utf8')
|
|
139
|
-
const mdRuleListCur = fs.readFileSync(ruleListDocsPath, 'utf8')
|
|
140
|
-
|
|
141
|
-
// Get rules table
|
|
142
|
-
|
|
143
|
-
const header = '# @sap/eslint-plugin-cds [latest]\n\n'
|
|
144
|
-
const mdRuleList = genMdRules(rules, release)
|
|
145
|
-
|
|
146
|
-
// Get rule details
|
|
147
|
-
let mdRules = ''
|
|
148
|
-
|
|
149
|
-
rules.forEach(rule => {
|
|
150
|
-
mdRules += `${rule.contents}\n\n${rule.sources}\n\n---\n\n`
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
if (mdRuleListCur !== mdRuleList || mdRulesCur !== mdRules) {
|
|
154
|
-
fs.writeFileSync(ruleDocsPath, header + mdRules, 'utf8')
|
|
155
|
-
fs.writeFileSync(ruleListDocsPath, header + mdRuleList, 'utf8')
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function getPackageVersion (registry) {
|
|
160
|
-
let result
|
|
161
|
-
try {
|
|
162
|
-
result = cp
|
|
163
|
-
.execSync(`npm show @sap/eslint-plugin-cds --@sap:registry=${registry} --json`, {
|
|
164
|
-
cwd: process.cwd(),
|
|
165
|
-
shell: IS_WIN,
|
|
166
|
-
stdio: 'pipe'
|
|
167
|
-
})
|
|
168
|
-
.toString()
|
|
169
|
-
} catch (_err) {
|
|
170
|
-
LOG?.(`Failed to connect to ${registry} - check your connection and try again.`)
|
|
171
|
-
exit(0)
|
|
172
|
-
}
|
|
173
|
-
const version = JSON.parse(result).version
|
|
174
|
-
if (!version) {
|
|
175
|
-
LOG?.(`Failed to get latest plugin version from ${registry} - check your connection and try again.`)
|
|
176
|
-
exit(0)
|
|
177
|
-
}
|
|
178
|
-
return version
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function getRules (docsPath, rulePath, testPath, versionRequired = '0.0.0', release = false) {
|
|
182
|
-
let mdRule, mdRuleSources, mdRuleContents
|
|
183
|
-
const rules = []
|
|
184
|
-
const ruleVersionsPath = path.join(docsPath, '_data', 'rule_versions.json')
|
|
185
|
-
const ruleVersions = require(ruleVersionsPath)
|
|
186
|
-
let fileNumber = 0
|
|
187
|
-
fs.readdirSync(rulePath).filter((file) => {
|
|
188
|
-
if (path.extname(file).toLowerCase() === '.js' && file !== 'index.js') {
|
|
189
|
-
const rule = path.basename(file).replace(path.extname(file), '')
|
|
190
|
-
const ruleTestPath = path.join(testPath, rule, 'rule.test.js')
|
|
191
|
-
fileNumber++
|
|
192
|
-
|
|
193
|
-
// Get rule meta information
|
|
194
|
-
const ruleMeta = require(path.join(rulePath, file)).meta
|
|
195
|
-
let version = ruleVersions.added[rule]
|
|
196
|
-
if (!version) {
|
|
197
|
-
version = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()).version
|
|
198
|
-
ruleVersions.added[rule] = version
|
|
199
|
-
fs.writeFileSync(ruleVersionsPath, JSON.stringify(ruleVersions, null, 4), 'utf8')
|
|
200
|
-
}
|
|
201
|
-
if ((release && semver.satisfies(version, `<=${versionRequired}`)) || !release) {
|
|
202
|
-
LOG?.(`${fileNumber}> preparing docs for ${ruleTestPath}`)
|
|
203
|
-
|
|
204
|
-
const details = ruleMeta.docs.description
|
|
205
|
-
const flavor = ruleMeta.model ? ruleMeta.model : constants.DEFAULT_RULE_FLAVOR
|
|
206
|
-
const category = (flavor === 'none') ? 'Environment' : 'Model Validation'
|
|
207
|
-
const fixable = ruleMeta.fixable
|
|
208
|
-
const messages = ruleMeta.messages ? ruleMeta.messages : []
|
|
209
|
-
const recommended = ruleMeta.docs.recommended
|
|
210
|
-
const suggestions = ruleMeta.hasSuggestions
|
|
211
|
-
|
|
212
|
-
let underConstruction = ''
|
|
213
|
-
if (!release && (version === 'TBD' || semver.satisfies(version, `>${versionRequired}`))) {
|
|
214
|
-
underConstruction = '🚧'
|
|
215
|
-
LOG?.(` > 🚧 Rule '${rule}' still under construction.\n`)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const isFixable = ['code', 'whitespace'].includes(fixable) ? '🔧' : ''
|
|
219
|
-
const isRecommended = recommended === true ? '✔️' : ''
|
|
220
|
-
const hasSuggestions = suggestions === true ? '💡' : ''
|
|
221
|
-
|
|
222
|
-
const rulesEntry = {
|
|
223
|
-
name: rule,
|
|
224
|
-
details,
|
|
225
|
-
recommended: isRecommended,
|
|
226
|
-
fixable: isFixable,
|
|
227
|
-
hasSuggestions,
|
|
228
|
-
construction: underConstruction,
|
|
229
|
-
messages,
|
|
230
|
-
version
|
|
231
|
-
}
|
|
232
|
-
try {
|
|
233
|
-
mdRule = getRuleExamples(rule, ruleTestPath, testPath, rulesEntry)
|
|
234
|
-
} catch (_err) {
|
|
235
|
-
// Just continue
|
|
236
|
-
}
|
|
237
|
-
mdRuleContents = ''
|
|
238
|
-
|
|
239
|
-
mdRuleContents +=
|
|
240
|
-
!release && underConstruction
|
|
241
|
-
? `## ${rule}\n<span class='shifted'>${underConstruction} <span class='label'>${category}</span></span>\n\n`
|
|
242
|
-
: `## ${rule}\n<span class='shifted label'>${category}</span>\n\n`
|
|
243
|
-
|
|
244
|
-
mdRuleContents += `### Rule Details\n${details}\n\n`
|
|
245
|
-
if (mdRule) {
|
|
246
|
-
mdRuleContents += `### Examples\n${mdRule}\n\n`
|
|
247
|
-
}
|
|
248
|
-
mdRuleContents += `### Version\nThis rule was introduced in \`@sap/eslint-plugin-cds ${version}\`.\n\n`
|
|
249
|
-
mdRuleSources = `### Resources\n[Rule & Documentation source](${path
|
|
250
|
-
.relative(docsPath, path.join(rulePath, `${rule}.js`))
|
|
251
|
-
.replace(/\\/g, '/')})\n\n`
|
|
252
|
-
|
|
253
|
-
rulesEntry.contents = mdRuleContents
|
|
254
|
-
rulesEntry.sources = mdRuleSources
|
|
255
|
-
rules.push(rulesEntry)
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return undefined
|
|
259
|
-
})
|
|
260
|
-
return rules
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function getRuleExamples (rule, ruleTestPath, testPath, ruleDictEntry) {
|
|
264
|
-
// Get rule valid/invalid tests
|
|
265
|
-
let mdRule = ''
|
|
266
|
-
if (fs.existsSync(ruleTestPath)) {
|
|
267
|
-
require(`${ruleTestPath}`)
|
|
268
|
-
const testerCases = Cache.get(`testerCases:${rule}`)
|
|
269
|
-
const isEnvRule = testerCases.valid[0].filename === '<text>'
|
|
270
|
-
const valid = !isEnvRule ? fs.readFileSync(testerCases.valid[0].filename, 'utf8') : JSON.stringify(testerCases.valid[0].options[0].environment, null, 4)
|
|
271
|
-
const invalid = !isEnvRule ? fs.readFileSync(testerCases.invalid[0].filename, 'utf8') : JSON.stringify(testerCases.invalid[0].options[0].environment, null, 4)
|
|
272
|
-
let validString = ''
|
|
273
|
-
let invalidString = ''
|
|
274
|
-
if (!isEnvRule) {
|
|
275
|
-
const errors = testerCases.invalid[0].errors
|
|
276
|
-
let errorsSorted = []
|
|
277
|
-
errors.forEach((err) => {
|
|
278
|
-
if (errorsSorted.length === 0) {
|
|
279
|
-
errorsSorted = [err]
|
|
280
|
-
} else {
|
|
281
|
-
const errLast = errorsSorted[errorsSorted.length - 1]
|
|
282
|
-
if (err.line > errLast.line) {
|
|
283
|
-
errorsSorted.push(err)
|
|
284
|
-
} else if (err.line < errLast.line) {
|
|
285
|
-
errorsSorted.unshift(err)
|
|
286
|
-
} else {
|
|
287
|
-
if (err.column > errLast.column) {
|
|
288
|
-
errorsSorted.push(err)
|
|
289
|
-
} else if (err.line < errLast.line) {
|
|
290
|
-
errorsSorted.unshift(err)
|
|
291
|
-
} else {
|
|
292
|
-
if (err.messageId) {
|
|
293
|
-
errorsSorted[errorsSorted.length - 1].messageId += '\n' + err.messageId
|
|
294
|
-
}
|
|
295
|
-
if (err.message) {
|
|
296
|
-
errorsSorted[errorsSorted.length - 1].message += '\n' + err.message
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
})
|
|
302
|
-
const code = invalid.split('\n')
|
|
303
|
-
errorsSorted.forEach((err, i) => {
|
|
304
|
-
if (err.messageId && ruleDictEntry.messages) {
|
|
305
|
-
let msg = ruleDictEntry.messages[err.messageId]
|
|
306
|
-
let data
|
|
307
|
-
if (errorsSorted[i].suggestions && errorsSorted[i].suggestions[0]) {
|
|
308
|
-
data = errorsSorted[i].suggestions[0].data
|
|
309
|
-
}
|
|
310
|
-
if (data && msg) {
|
|
311
|
-
Object.keys(data).forEach((d) => {
|
|
312
|
-
msg = msg.replace(`{{${d}}}`, data[d])
|
|
313
|
-
})
|
|
314
|
-
}
|
|
315
|
-
err.message = msg
|
|
316
|
-
}
|
|
317
|
-
const msg = err.message && err.message.includes && err.message.includes('"') ? err.message.replace(/"/gm, '`') : err.message
|
|
318
|
-
if (err.line) {
|
|
319
|
-
const stringStart = errorsSorted[i - 1] ? errorsSorted[i - 1].line : 0
|
|
320
|
-
invalidString += code.slice(stringStart, err.line - 1).join('\n')
|
|
321
|
-
const errorString = err.line === err.endLine ? code[err.line - 1] : code.slice(err.line - 1, err.endLine - 1).join('\n')
|
|
322
|
-
const replacedErrorLine = errorString.substring(0, err.column - 1) +
|
|
323
|
-
`<span style="display:inline-block; position:relative; color:red; border-bottom:2pt dotted red" title="${msg}"><b><i>` +
|
|
324
|
-
errorString.substring(err.column - 1, err.endColumn - 1) +
|
|
325
|
-
'</i></b></span>' + errorString.substring(err.endColumn - 1)
|
|
326
|
-
invalidString += '\n' + replacedErrorLine
|
|
327
|
-
const stringEnd = errorsSorted[i + 1] ? code.slice(err.line, errorsSorted[i + 1].line - 1).join('\n') : code.slice(err.line).join('\n')
|
|
328
|
-
invalidString += errorsSorted[i + 1] ? stringEnd : '\n' + stringEnd
|
|
329
|
-
}
|
|
330
|
-
})
|
|
331
|
-
} else {
|
|
332
|
-
invalidString = invalid
|
|
333
|
-
}
|
|
334
|
-
validString = valid
|
|
335
|
-
|
|
336
|
-
mdRule +=
|
|
337
|
-
'<span>✔️ Example of ' +
|
|
338
|
-
'<span style="color:green">correct</span> ' +
|
|
339
|
-
`code for this rule:</span>\n\n<pre><code>${validString.trim()}</code></pre>\n\n`
|
|
340
|
-
mdRule +=
|
|
341
|
-
'<span>❌ Example of ' +
|
|
342
|
-
'<span style="color:red">incorrect</span> ' +
|
|
343
|
-
`code for this rule:</span>\n\n<pre><code>${invalidString.trim()}</code></pre>`
|
|
344
|
-
}
|
|
345
|
-
return mdRule
|
|
346
|
-
}
|