@sap/eslint-plugin-cds 2.1.0 → 2.3.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.
@@ -1,39 +1,31 @@
1
- const cdsLint = require("../../api");
2
-
3
- module.exports = cdsLint.createRule(
4
- /* Rule meta */
5
- {
1
+ module.exports = require("../../api").createRule({
2
+ meta: {
6
3
  docs: {
7
- description: `In relational databases, the foreign key information of a \`TO MANY\` relationship must be defined within the target and specified in an \`ON\` condition.
8
- Therefore, all \`TO MANY\` relationships must have a defined \`ON\` condition.`,
4
+ description: `Foreign key information of a \`TO MANY\` relationship must be defined within the target and specified in an \`ON\` condition.`,
9
5
  category: "Model Validation",
6
+ recommended: true,
10
7
  version: "2.1.0",
11
8
  },
9
+ severity: "error",
12
10
  type: "problem",
13
11
  },
14
- /* Rule logic */
15
- (report, cds) => {
16
- return createReport(report, cds) || report;
17
- }
18
- );
19
-
20
- const createReport = (report, cds) => {
21
- const m = cds.model;
22
- m.forall((d) => {
23
- if (d.name) {
24
- if (!d.elements) return;
25
- for (const elementName in d.elements) {
26
- const element = d.elements[elementName];
27
- if (element.is2many && !element.on) {
28
- const loc = cds.getLocation(elementName, element);
29
- report.push({
30
- message: `You must provide an \`ON\` condition for \`TO MANY\` relationship '${element.name}'.`,
31
- loc,
32
- file: d.$location.file,
33
- });
12
+ create: function (context) {
13
+ const m = context.cds.model; if (!m) return
14
+ m.forall((d) => {
15
+ if (d.name) {
16
+ if (!d.elements) return;
17
+ for (const elementName in d.elements) {
18
+ const element = d.elements[elementName];
19
+ if (element.is2many && !element.on) {
20
+ const loc = context.cds.getLocation(elementName, element);
21
+ context.report({
22
+ message: `You must provide an \`ON\` condition for \`TO MANY\` relationship '${element.name}'.`,
23
+ loc,
24
+ file: d.$location.file,
25
+ });
26
+ }
34
27
  }
35
28
  }
36
- }
37
- });
38
- return report;
39
- };
29
+ });
30
+ },
31
+ });
@@ -1,26 +1,20 @@
1
- const cdsLint = require("../../lib/api");
2
-
3
- module.exports = cdsLint.createRule(
4
- {
1
+ // @ts-check
2
+ module.exports = require("../../api").createRule({
3
+ meta: {
5
4
  docs: {
6
- description: "{{description}}",
7
- category: "{{category}}",
8
- {{recommended}}
9
- {{version}}
5
+ description: "{{description}}",
6
+ version: "{{version}}"
10
7
  },
11
- type: "{{type}}",
12
- {{messages}}
8
+ type:"{{type}}",
13
9
  },
14
- (cds, model, env, context) => {
15
- return createReport(report, cds, {{cds_object}}, report) || report;
10
+ create: function(context) {
11
+ const m = cotext.cds.model;
12
+ m.forall((d)) => {
13
+ // Add cds logic here, for example
14
+ return [{
15
+ message: "{{messages}}",
16
+ loc: {{loc}}
17
+ }];
18
+ }
16
19
  }
17
- );
18
-
19
- const createReport = (report, cds, {{cds_object}}, report) => {
20
- // Iterate over your model using m.foreach or m.forall...
21
- m.forall((d) => {
22
- // Add reports when rule is triggered
23
- report.push({ mesage: {{error_msg}}, loc, file })
24
- })
25
- return report;
26
- };
20
+ })
@@ -1,55 +1,52 @@
1
- const cdsLint = require("../../api");
2
-
3
- module.exports = cdsLint.createRule(
4
- /* Rule meta */
5
- {
1
+ module.exports = require("../../api").createRule({
2
+ meta: {
6
3
  docs: {
7
- description: "Should make suggestions for possible missing sql casts.",
4
+ description: "Should make suggestions for possible missing SQL casts.",
8
5
  category: "Model Validation",
6
+ recommended: true,
9
7
  version: "1.0.8",
10
8
  },
9
+ severity: "warn",
11
10
  type: "suggestion",
11
+ hasSuggestions: true,
12
12
  messages: {
13
- missingSQLCast: "Missing SQL cast?",
13
+ missingSQLCast:
14
+ "Potential issue - Missing SQL cast for column expression?",
14
15
  },
15
16
  },
16
- /* Rule logic */
17
- (report, cds, sourcecode) => {
18
- return createReport(report, cds, sourcecode) || report;
19
- }
20
- );
21
-
22
- const createReport = (report, cds, sourcecode) => {
23
- const m = cds.model;
24
- if (m) {
25
- const view = (d) => d.query;
26
- m.foreach(view, (v) => {
27
- if (v.query.SET)
28
- for (const { SELECT } of v.query.SET.args) {
29
- // Only in UNION cases?
30
- for (const each of SELECT.columns || []) {
31
- const { xpr, cast, $location: location } = each;
32
- if (cast && xpr) {
33
- if (xpr[0].xpr && xpr[0].xpr && xpr[0].cast) {
34
- continue;
35
- } else {
36
- if (sourcecode.lines[location.line - 1]) {
37
- const endCol = sourcecode.lines[location.line - 1].length;
38
- const loc = {
39
- start: { line: location.line, column: location.col - 1 },
40
- end: { line: location.line, column: endCol },
41
- };
42
- report.push({
43
- message: `Potential issue - Missing SQL cast for column expression?`,
44
- loc,
45
- file: location.file,
46
- });
17
+ create: function (context) {
18
+ const m = context.cds.model;
19
+ if (m) {
20
+ const view = (d) => d.query;
21
+ m.foreach(view, (v) => {
22
+ if (v.query.SET)
23
+ for (const { SELECT } of v.query.SET.args) {
24
+ // Only in UNION cases?
25
+ for (const each of SELECT.columns || []) {
26
+ const { xpr, cast, $location: location } = each;
27
+ if (cast && xpr) {
28
+ if (xpr[0].xpr && xpr[0].xpr && xpr[0].cast) {
29
+ continue;
30
+ } else {
31
+ if (context.sourcecode.lines[location.line - 1]) {
32
+ const endCol =
33
+ context.sourcecode.lines[location.line - 1].length;
34
+ const loc = {
35
+ start: { line: location.line, column: location.col - 1 },
36
+ end: { line: location.line, column: endCol },
37
+ };
38
+ context.report({
39
+ messageId: "missingSQLCast",
40
+ loc,
41
+ file: location.file,
42
+ });
43
+ }
47
44
  }
48
45
  }
49
46
  }
50
47
  }
51
- }
52
- });
53
- }
54
- return report;
55
- };
48
+ });
49
+ }
50
+ return context.report;
51
+ },
52
+ });
@@ -1,69 +1,75 @@
1
- const cdsLint = require("../../api");
2
-
3
- module.exports = cdsLint.createRule(
4
- /* Rule meta */
5
- {
1
+ module.exports = require("../../api").createRule({
2
+ meta: {
6
3
  docs: {
7
- description: `Regular element names should all be in lowercase.`,
4
+ description: "Regular element names should start with lowercase letters.",
8
5
  category: "Model Validation",
9
6
  version: "1.0.4",
10
7
  },
11
8
  type: "suggestion",
9
+ hasSuggestions: true,
12
10
  messages: {
13
- startLowercase: "Start elements with lowercase letters",
11
+ startLowercase:
12
+ "Element name '{{entityName}}.{{elementName}}' should start with a lowercase letter.",
13
+ fixLowercase:
14
+ "Start element name with a lowercase letter."
14
15
  },
15
16
  fixable: "code",
16
17
  },
17
- /* Rule logic */
18
- (report, cds, sourcecode) => {
19
- return createReport(report, cds, sourcecode) || report;
20
- }
21
- );
22
-
23
- const createReport = (report, cds, sourcecode) => {
24
- const m = cds.model;
25
- if (m && m.definitions) {
26
- m.forall((d) => {
27
- const entityName = d.name;
28
- for (const elementName in d.elements) {
29
- const element = d.elements[elementName];
30
- if (
31
- elementName &&
32
- !(entityName.startsWith("localized") || entityName.endsWith("texts"))
33
- ) {
18
+ create: function (context) {
19
+ const m = context.cds.model;
20
+ if (m && m.definitions) {
21
+ m.forall((d) => {
22
+ const entityName = d.name;
23
+ for (const elementName in d.elements) {
24
+ const element = d.elements[elementName];
34
25
  if (
35
- elementName.charAt(0) !== elementName.charAt(0).toLowerCase() &&
36
- !["ID"].includes(elementName)
26
+ elementName &&
27
+ !(
28
+ entityName.startsWith("localized") || entityName.endsWith("texts")
29
+ )
37
30
  ) {
38
- if (element.$location && element.$location.file) {
39
- const file = element.$location.file;
40
- const loc = cds.getLocation(elementName, element);
41
- const fix = (fixer) => {
42
- const elementNameSanitized =
43
- elementName.charAt(0).toLowerCase() + elementName.slice(1);
44
- const rangeEnd = sourcecode.getIndexFromLoc({
45
- line: loc.end.line,
46
- column: loc.end.column,
31
+ if (
32
+ elementName.charAt(0) !== elementName.charAt(0).toLowerCase() &&
33
+ !["ID"].includes(elementName)
34
+ ) {
35
+ if (element.$location && element.$location.file) {
36
+ const file = element.$location.file;
37
+ const loc = context.cds.getLocation(elementName, element);
38
+ const fix = (fixer) => {
39
+ const elementNameSanitized =
40
+ elementName.charAt(0).toLowerCase() + elementName.slice(1);
41
+ const rangeEnd = context.sourcecode.getIndexFromLoc({
42
+ line: loc.end.line,
43
+ column: loc.end.column,
44
+ });
45
+ const rangeBeg = rangeEnd
46
+ ? rangeEnd - elementNameSanitized.length
47
+ : 0;
48
+ return fixer.replaceTextRange(
49
+ [rangeBeg, rangeEnd],
50
+ elementNameSanitized
51
+ );
52
+ };
53
+ context.report({
54
+ messageId: "startLowercase",
55
+ loc,
56
+ file,
57
+ data: {
58
+ entityName,
59
+ elementName,
60
+ },
61
+ suggest: [
62
+ {
63
+ messageId: "fixLowercase",
64
+ fix,
65
+ },
66
+ ],
47
67
  });
48
- const rangeBeg = rangeEnd
49
- ? rangeEnd - elementNameSanitized.length
50
- : 0;
51
- return fixer.replaceTextRange(
52
- [rangeBeg, rangeEnd],
53
- elementNameSanitized
54
- );
55
- };
56
- report.push({
57
- message: `Element '${entityName}.${elementName}' must be lowercase`,
58
- loc,
59
- fix,
60
- file,
61
- });
68
+ }
62
69
  }
63
70
  }
64
71
  }
65
- }
66
- });
67
- }
68
- return report;
69
- };
72
+ });
73
+ }
74
+ },
75
+ });
@@ -1,31 +1,23 @@
1
- const cdsLint = require("../../api");
2
-
3
- module.exports = cdsLint.createRule(
4
- /* Rule meta */
5
- {
1
+ module.exports = require("../../api").createRule({
2
+ meta: {
6
3
  docs: {
7
- description: "Entity names should all be in uppercase.",
4
+ description: "Regular entity names should start with uppercase letters.",
8
5
  category: "Model Validation",
9
6
  version: "1.0.4",
10
7
  },
11
8
  type: "suggestion",
9
+ hasSuggestions: true,
12
10
  messages: {
13
- startUppercase: "Start entity and type names with capital letters",
11
+ startUppercase:
12
+ "Entity name '{{entityName}}' should start with an uppercase letter.",
13
+ fixUppercase: "Start entity name with an uppercase letter.",
14
14
  },
15
15
  fixable: "code",
16
16
  },
17
- /* Rule logic */
18
- (report, cds, sourcecode) => {
19
- return createReport(report, cds, sourcecode) || report;
20
- }
21
- );
22
-
23
- const createReport = (report, cds, sourcecode) => {
24
- const m = cds.model;
25
- if (m) {
26
- m.foreach(
27
- "entity",
28
- (e) => {
17
+ create: function (context) {
18
+ const m = context.cds.model;
19
+ if (m) {
20
+ m.foreach("entity", (e) => {
29
21
  let entityName = e.name;
30
22
  const names = entityName.split(".");
31
23
  entityName = names[names.length - 1];
@@ -35,41 +27,39 @@ const createReport = (report, cds, sourcecode) => {
35
27
  ) {
36
28
  if (entityName.charAt(0) !== entityName.charAt(0).toUpperCase()) {
37
29
  if (e.$location && e.$location.file) {
38
- const file = e.$location.file;
39
- const loc = cds.getLocation(entityName, e);
40
- const fix = (fixer) => {
41
- const entityNameSanitized =
42
- entityName.charAt(0).toUpperCase() + entityName.slice(1);
43
- const rangeEnd = sourcecode.getIndexFromLoc({
44
- line: loc.end.line,
45
- column: loc.end.column,
46
- });
47
- const rangeBeg = rangeEnd
48
- ? rangeEnd - entityNameSanitized.length
49
- : 0;
50
- return fixer.replaceTextRange(
51
- [rangeBeg, rangeEnd],
52
- entityNameSanitized
53
- );
54
- };
55
- const suggest = [
56
- {
30
+ const file = e.$location.file;
31
+ const loc = context.cds.getLocation(entityName, e);
32
+ const fix = (fixer) => {
33
+ const entityNameSanitized =
34
+ entityName.charAt(0).toUpperCase() + entityName.slice(1);
35
+ const rangeEnd = context.sourcecode.getIndexFromLoc({
36
+ line: loc.end.line,
37
+ column: loc.end.column,
38
+ });
39
+ const rangeBeg = rangeEnd
40
+ ? rangeEnd - entityNameSanitized.length
41
+ : 0;
42
+ return fixer.replaceTextRange(
43
+ [rangeBeg, rangeEnd],
44
+ entityNameSanitized
45
+ );
46
+ };
47
+ context.report({
57
48
  messageId: "startUppercase",
58
- fix,
59
- },
60
- ];
61
- report.push({
62
- message: `Entity '${entityName}' must be uppercase`,
63
- loc,
64
- file,
65
- fix,
66
- suggest,
67
- });
68
- }
49
+ loc,
50
+ file,
51
+ data: { entityName },
52
+ suggest: [
53
+ {
54
+ messageId: "fixUppercase",
55
+ fix,
56
+ },
57
+ ],
58
+ });
59
+ }
69
60
  }
70
61
  }
71
- }
72
- );
73
- }
74
- return report;
75
- };
62
+ });
63
+ }
64
+ },
65
+ });
@@ -0,0 +1,92 @@
1
+ const {basename, extname} = require('path')
2
+ const findFuzzy = require('../utils/fuzzySearch')
3
+ const SEP = '[,;\t]'
4
+ const EOL = '\\r?\\n'
5
+
6
+ module.exports = require("../../api").createRule({
7
+ meta: {
8
+ docs: {
9
+ description: `CSV files for entities must refer to valid element names.`,
10
+ category: "Model Validation",
11
+ recommended: true,
12
+ version: "2.3.0",
13
+ },
14
+ severity: "warn",
15
+ type: "problem",
16
+ hasSuggestions: true,
17
+ messages: {
18
+ InvalidColumn: `Invalid column '{{column}}'. Did you mean '{{candidates}}'?`,
19
+ ReplaceColumnWith: `Replace '{{column}}' with '{{candidates}}'`
20
+ }
21
+ },
22
+ create: function (context) {
23
+ const {cds, code, filePath, sourcecode} = context
24
+
25
+ if (!filePath.endsWith('.csv')) return
26
+ if (!cds.model) return
27
+ let {env, model} = cds;
28
+ model = cds.compile.for.sql(model, {names:env.sql.names, messages: []} )
29
+
30
+ const filename = basename(filePath)
31
+ const entityName = filename.replace(/-/g,'.').slice(0, -extname(filename).length)
32
+ const entity = _entity4(entityName, model)
33
+ if (!entity) return
34
+
35
+ const elements = Object.values(entity.elements)
36
+ .filter (e => !!e['@cds.persistence.name'])
37
+ .map (e => e['@cds.persistence.name'].toUpperCase())
38
+
39
+ const [ cols ] = cds.parse.csv(code)
40
+ const missing = cols.filter (col => !elements.includes(col.toUpperCase()))
41
+ missing.forEach(miss => {
42
+ const index = _findInCode (miss, code)
43
+ const loc = sourcecode.getLocFromIndex(index)
44
+ const candidates = findFuzzy(miss, Object.keys(entity.elements).sort())
45
+ const suggest = candidates.map(cand => { return {
46
+ messageId: 'ReplaceColumnWith',
47
+ data: {column: miss, candidates:cand},
48
+ fix: (fixer) => fixer.replaceTextRange([index, index+miss.length], cand)
49
+ }})
50
+ context.report({
51
+ messageId: 'InvalidColumn',
52
+ data: {column: miss, candidates},
53
+ loc: {start: loc, end: {line: loc.line, column: loc.column+miss.length}},
54
+ file: filePath,
55
+ suggest
56
+ })
57
+ })
58
+ }
59
+
60
+ })
61
+
62
+ function _findInCode (miss, code) {
63
+ // middle
64
+ let match = new RegExp(SEP+miss+SEP).exec(code)
65
+ if (match) return match.index+1
66
+ // end of line
67
+ match = new RegExp(SEP+miss+EOL).exec(code)
68
+ if (match) return match.index+1
69
+ // start of doc
70
+ match = new RegExp('^'+miss+SEP).exec(code)
71
+ if (match) return match.index
72
+ // somewhere (fallback)
73
+ return code.indexOf(miss)
74
+ }
75
+
76
+ function _entity4 (name, csn) {
77
+ let entity = csn.definitions [name]
78
+ if (!entity) {
79
+ if (/(.+)[._]texts_?/.test (name)) { // 'Books.texts', 'Books.texts_de'
80
+ const base = csn.definitions [RegExp.$1]
81
+ return base && _entity4 (base.elements.texts.target, csn)
82
+ }
83
+ else return
84
+ }
85
+ // we also support simple views if they have no projection
86
+ const p = entity.query && entity.query.SELECT || entity.projection
87
+ if (p && !p.columns && p.from.ref && p.from.ref.length === 1) {
88
+ if (csn.definitions [p.from.ref[0]]) return entity
89
+ }
90
+ return entity.name ? entity : { name, __proto__:entity }
91
+ }
92
+
@@ -0,0 +1,48 @@
1
+ import { Rule, RuleTester, SourceCode } from "eslint";
2
+
3
+ export interface CDSRuleMetaData extends Rule.RuleMetaData {
4
+ docs: {
5
+ /** provides the short description of the rule in the [rules index](https://eslint.org/docs/rules/) */
6
+ description: Rule.RuleMetaData['docs']['description'];
7
+ /** specifies version of @sap/eslint-plugin-cds at which the rule was first implemented */
8
+ version: string;
9
+ };
10
+ messages?: Rule.RuleMetaData['messages'];
11
+ fixable?: Rule.RuleMetaData['fixable'];
12
+ schema?: Rule.RuleMetaData['schema'];
13
+ deprecated?: Rule.RuleMetaData['deprecated'];
14
+ type?: Rule.RuleMetaData['type'];
15
+ }
16
+
17
+ export type CDSReport = Rule.ReportDescriptor & { file?: string };
18
+
19
+ export interface CDSRuleContext extends Rule.RuleContext {
20
+ cds: any;
21
+ configPath: string;
22
+ code: string;
23
+ filePath: string;
24
+ options: [];
25
+ ruleID: string;
26
+ sourcecode: SourceCode
27
+ }
28
+ export interface CDSRuleSpec {
29
+ meta: CDSRuleMetaData,
30
+ create: (arg0: CDSRuleContext) => Rule.ReportDescriptor;
31
+ }
32
+
33
+ export interface CDSTestCaseError extends RuleTester.TestCaseError {
34
+ message: string | RegExp;
35
+ }
36
+
37
+ export interface CDSRuleTestOpts {
38
+ /** specifies __dirname */
39
+ root: string;
40
+ /** requires your rule .js here */
41
+ rule?: string;
42
+ /** filename ('schema.cds' for model, 'package.json' for env) */
43
+ filename: string;
44
+ /** resolves cds parser path */
45
+ parser?: string;
46
+ /** List of warnings/errors from ESLint's [ruleTester](https://eslint.org/docs/developer-guide/nodejs-api#ruletester) */
47
+ errors: CDSTestCaseError[]
48
+ }