api-quality-spectral-ruleset 1.0.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.
@@ -0,0 +1,35 @@
1
+
2
+ const isVariable = (part) => {
3
+ return (part.startsWith('{') && part.endsWith('}'));
4
+ }
5
+
6
+ module.exports = (given, { except }, context) => {
7
+ const result = [];
8
+ const paths = given || [];
9
+ if (paths.length === 0) return result;
10
+
11
+ const parts = paths.substr(1).split('/');
12
+ let previousIsVar = isVariable(parts.shift());
13
+ if (previousIsVar) {
14
+ return [{
15
+ message: context.rule.message,
16
+ }];
17
+ }
18
+
19
+ for (const part of parts) {
20
+ if (except && except.includes(part)) {
21
+ previousIsVar = true;
22
+ continue;
23
+ }
24
+
25
+ const currentIsVariable = isVariable(part);
26
+ if (currentIsVariable === previousIsVar) {
27
+ return [{
28
+ message: context.rule.message,
29
+ }];
30
+ }
31
+ previousIsVar = currentIsVariable;
32
+ }
33
+
34
+ return result;
35
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @param {string} given
3
+ * @param {object} options
4
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
5
+ */
6
+ module.exports = (parameters, options, context) => {
7
+ if (!parameters || !Array.isArray(parameters) || parameters.length === 0) {
8
+ return [];
9
+ }
10
+
11
+ const bodyParams = parameters.filter(param => {
12
+ return param && typeof param === 'object' && param.in === 'body';
13
+ });
14
+
15
+ if (bodyParams.length > 1) {
16
+ return [
17
+ {
18
+ message: context.rule.message,
19
+ }
20
+ ];
21
+ }
22
+
23
+ return [];
24
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @param {object} given
3
+ * @param {object} options
4
+ * @param {string} options.property
5
+ * @param {string} options.equalTo
6
+ * @param {string} options.result
7
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
8
+ *
9
+ */
10
+ module.exports = (given, options, context) => {
11
+ const errors = [];
12
+ if (!given) {
13
+ return errors;
14
+ }
15
+
16
+ const propA = given[options.property];
17
+ const propB = given[options.equalTo];
18
+
19
+ if (
20
+ typeof propA === 'string' &&
21
+ typeof propB === 'string' &&
22
+ options.result === 'falsy' &&
23
+ propA.trim().toUpperCase() === propB.trim().toUpperCase()
24
+ ) {
25
+ errors.push({
26
+ message: context.rule.message,
27
+ path: [...context.path, options.property]
28
+ });
29
+ }
30
+
31
+ return errors;
32
+ };
33
+
@@ -0,0 +1,88 @@
1
+ // options:
2
+ // except: 'array<string>'
3
+ // schema: object
4
+
5
+ function DeepCompare() {
6
+ var i, l, leftChain, rightChain;
7
+
8
+ function Objects(x, y, required = []) {
9
+ let p;
10
+
11
+ // Quick checking of one object being a subset of another.
12
+ // todo: cache the structure of arguments[0] for performance
13
+ for (p in y) {
14
+ if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
15
+ if (required.includes(p)) {
16
+ return false;
17
+ }
18
+ } else if (typeof y[p] !== typeof x[p]) {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ for (p in x) {
24
+ if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
25
+ if (required.includes(p)) {
26
+ return false;
27
+ }
28
+ continue;
29
+ } else if (typeof y[p] !== typeof x[p]) {
30
+ return false;
31
+ }
32
+
33
+ switch (typeof x[p]) {
34
+ case "object":
35
+ case "function":
36
+ leftChain.push(x);
37
+ rightChain.push(y);
38
+
39
+ if (!Objects(x[p], y[p], x[p].required || required)) {
40
+ return false;
41
+ }
42
+
43
+ leftChain.pop();
44
+ rightChain.pop();
45
+ break;
46
+
47
+ default:
48
+ if (x[p] !== y[p]) {
49
+ return false;
50
+ }
51
+ break;
52
+ }
53
+ }
54
+
55
+ return true;
56
+ }
57
+
58
+ for (i = 1, l = arguments.length; i < l; i++) {
59
+ leftChain = []; //Todo: this can be cached
60
+ rightChain = [];
61
+
62
+ if (!Objects(arguments[0], arguments[i], arguments[0].required || [])) {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ return true;
68
+ }
69
+
70
+ /**
71
+ * @param {Array<string>} given
72
+ * @param {object} options
73
+ * @param {Array<string>} options.except
74
+ * @param {object} options.schema
75
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
76
+ */
77
+ module.exports = (given, options, context) => {
78
+ const result = [];
79
+ const paths = given || [];
80
+ if (paths.length === 0) return result;
81
+ if (options.except.includes(String(context.path[1]))) return result;
82
+
83
+ if (DeepCompare(options.schema, given)) return result;
84
+
85
+ return [{
86
+ message: context.rule.message,
87
+ }];
88
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @param {object} given
3
+ * @param {object} options
4
+ * @param {string} options.type
5
+ * @param {string} options.match
6
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
7
+ */
8
+ module.exports = (given, options, context) => {
9
+ const errors = [];
10
+ if (!given) return errors;
11
+
12
+ if (!given.type || given.type.toString() !== options.type.toString()) {
13
+ errors.push({
14
+ message: context.rule.message,
15
+ });
16
+ } else if (!given.default || given.default.toString() !== options.match.toString()) {
17
+ errors.push({
18
+ message: context.rule.message,
19
+ });
20
+ }
21
+
22
+ return errors;
23
+ }
@@ -0,0 +1,21 @@
1
+ const { pattern } = require("@stoplight/spectral-functions");
2
+
3
+ const patterns = {
4
+ kebabCase: /^(\/|[a-z0-9-.]+|{[a-zA-Z0-9_]+})+$/,
5
+ camelCase: /^[a-z]+([A-Z][a-z0-9]+)*$/,
6
+ snakeCase: /^[a-z]+(_[a-z0-9]+)*$/,
7
+ pascalCase: /^[A-Z]+([A-Z][a-z0-9]+)*$/
8
+ };
9
+
10
+ /**
11
+ * @param {object} given
12
+ * @param {object} options
13
+ * @param {string} options.pattern
14
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
15
+ */
16
+ module.exports = (given, options, context) => {
17
+ const errors = [];
18
+ if (!given) return errors;
19
+
20
+ return pattern(given, { match: patterns[options.pattern] }, context);
21
+ }
@@ -0,0 +1,76 @@
1
+ const NAMING_REGEX = {
2
+ "snake_case": /^[a-z0-9_$]*$/,
3
+ "kebab-case": /^[a-z0-9-]*$/,
4
+ "camelCase": /^[a-z]+([A-Z][a-z]+)*([A-Z])?$/,
5
+ "UpperCamelCase": /^[A-Z][a-z]+(?:[A-Z][a-z]+)*$/
6
+ };
7
+
8
+ const NAME_EXCEPTIONS = new Set([
9
+ "$init", "$start", "$limit", "$total", "$expand", "$orderby", "$select", "$exclude", "$filter"
10
+ ]);
11
+
12
+ const PARAM_REGEX = /\{([^}{]*)\}/g;
13
+
14
+ /**
15
+ * @param {string} given
16
+ * @param {object} options
17
+ * @param {string} options.namingConvention
18
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
19
+ */
20
+ module.exports = (given, options, context) => {
21
+ if (!given) return [];
22
+
23
+ const convention = options.namingConvention || "snake_case";
24
+ const regex = NAMING_REGEX[convention];
25
+
26
+ if (!regex) {
27
+ throw new Error(
28
+ `OAR012: Unknown naming convention "${convention}". ` +
29
+ `Allowed values: ${Object.keys(NAMING_REGEX).join(", ")}`
30
+ );
31
+ }
32
+
33
+ if (NAME_EXCEPTIONS.has(given)) {
34
+ return [];
35
+ }
36
+
37
+ let name = given;
38
+
39
+ // Strip parameter names
40
+ name = name.replace(PARAM_REGEX, (_, match) => match); // Keep only the parameter name without braces
41
+
42
+ switch (convention) {
43
+ case "camelCase":
44
+ case "UpperCamelCase":
45
+ name = name.replace(/\//g, "");
46
+ if (name.includes("_") || name.includes("-")) {
47
+ return [
48
+ {
49
+ message: `OAR012: "${given}" must follow ${convention}`,
50
+ path: context.path
51
+ }
52
+ ];
53
+ }
54
+ break;
55
+
56
+ case "kebab-case":
57
+ name = name.replace(/\//g, "-");
58
+ break;
59
+
60
+ case "snake_case":
61
+ default:
62
+ name = name.replace(/\//g, "_");
63
+ break;
64
+ }
65
+
66
+ if (!regex.test(name)) {
67
+ return [
68
+ {
69
+ message: `OAR012: "${given}" must follow ${convention}`,
70
+ path: context.path
71
+ }
72
+ ];
73
+ }
74
+
75
+ return [];
76
+ };
@@ -0,0 +1,29 @@
1
+ // Check that format is valid for a schema type.
2
+
3
+ /**
4
+ * @param {object} given
5
+ * @param {object} options
6
+ * @param {Array<string>} options.formats
7
+ * @param {Array<string>} options.properties
8
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
9
+ */
10
+ module.exports = function checkTypeAndFormat(given, options, context) {
11
+ if (given === null || typeof given !== "object") {
12
+ return [];
13
+ }
14
+
15
+ const errors = [];
16
+ const path = context.path || [];
17
+ const name = path.slice(-1)[0].toString();
18
+
19
+ if (options.properties.includes(name)) {
20
+ if (!given.format || !options.formats.includes(given.format)) {
21
+ errors.push({
22
+ message: context.rule.message,
23
+ path: [...path, name]
24
+ });
25
+ }
26
+ }
27
+
28
+ return errors;
29
+ };
@@ -0,0 +1,117 @@
1
+ const DEFAULT_ALLOWED_PATTERNS = `
2
+ ;get:^/[^/{}]+$
3
+ ;get:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+$
4
+ ;get:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+$
5
+ ;get:^/[^/{}]+/(\\{[^/{}]+\\}|me)$
6
+ ;get:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)$
7
+ ;get:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)$
8
+
9
+ ;post:^/[^/{}]+$
10
+ ;post:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+$
11
+ ;post:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+$
12
+ ;post:^/[^/{}]+/get$
13
+ ;post:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/get$
14
+ ;post:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/get$
15
+ ;post:^/[^/{}]+/delete$
16
+ ;post:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/delete$
17
+ ;post:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/delete$
18
+
19
+ ;put:^/[^/{}]+/(\\{[^/{}]+\\}|me)$
20
+ ;put:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)$
21
+ ;put:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)$
22
+
23
+ ;patch:^/[^/{}]+/(\\{[^/{}]+\\}|me)$
24
+ ;patch:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)$
25
+ ;patch:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)$
26
+
27
+ ;delete:^/[^/{}]+/(\\{[^/{}]+\\}|me)$
28
+ ;delete:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)$
29
+ ;delete:^/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)/[^/{}]+/(\\{[^/{}]+\\}|me)$
30
+ `;
31
+
32
+ const SUPPORTED_VERBS = ['get', 'post', 'put', 'patch', 'delete'];
33
+ const CONFIG_ERROR_PREFIX = 'OAR018:';
34
+
35
+ const parseAllowedPatterns = (patterns) => {
36
+ if (typeof patterns !== 'string') {
37
+ throw new Error(
38
+ `${CONFIG_ERROR_PREFIX} allowed-resources-paths must be a string`
39
+ );
40
+ }
41
+
42
+ return patterns
43
+ .split(';')
44
+ .map(p => p.trim())
45
+ .filter(Boolean)
46
+ .map(entry => {
47
+ const idx = entry.indexOf(':');
48
+
49
+ if (idx === -1) {
50
+ throw new Error(
51
+ `${CONFIG_ERROR_PREFIX} Invalid entry "${entry}". Expected format "<verb>:<regex>"`
52
+ );
53
+ }
54
+
55
+ const verb = entry.slice(0, idx).toLowerCase();
56
+ const regexSource = entry.slice(idx + 1);
57
+
58
+ if (!SUPPORTED_VERBS.includes(verb)) {
59
+ throw new Error(
60
+ `${CONFIG_ERROR_PREFIX} Unsupported HTTP verb "${verb}" in "${entry}"`
61
+ );
62
+ }
63
+
64
+ try {
65
+ return {
66
+ verb,
67
+ regex: new RegExp(regexSource),
68
+ };
69
+ } catch (err) {
70
+ throw new Error(
71
+ `${CONFIG_ERROR_PREFIX} Invalid regex for verb "${verb}": ${regexSource}. ${err.message}`
72
+ );
73
+ }
74
+ });
75
+ };
76
+
77
+ module.exports = (given, functionOptions = {}, context) => {
78
+ const results = [];
79
+ if (!given || typeof given !== 'object') return results;
80
+
81
+ let allowedPatterns;
82
+
83
+ try {
84
+ const patterns = functionOptions['allowed-resources-paths']?.trim()
85
+ ? functionOptions['allowed-resources-paths']
86
+ : DEFAULT_ALLOWED_PATTERNS;
87
+
88
+ allowedPatterns = parseAllowedPatterns(patterns);
89
+ } catch (error) {
90
+ return [{
91
+ message: error.message,
92
+ path: context.path,
93
+ }];
94
+ }
95
+
96
+ Object.entries(given).forEach(([path, operations]) => {
97
+ Object.keys(operations || {}).forEach(verb => {
98
+ const verbLower = verb.toLowerCase();
99
+ if (!SUPPORTED_VERBS.includes(verbLower)) return;
100
+
101
+ const isAllowed = allowedPatterns.some(p =>
102
+ p.verb === verbLower && p.regex.test(path)
103
+ );
104
+
105
+ if (!isAllowed) {
106
+ results.push({
107
+ message: context.rule.message
108
+ .replace('{{path}}', path)
109
+ .replace('{{verb}}', verbLower.toUpperCase()),
110
+ path: [...context.path, path, verb],
111
+ });
112
+ }
113
+ });
114
+ });
115
+
116
+ return results;
117
+ };
@@ -0,0 +1,26 @@
1
+
2
+ module.exports = (given, options, context) => {
3
+ const result = [];
4
+ if (!given) return result;
5
+
6
+ if (options.verbs !== '*' && !Object.keys(given).some(key => options.verbs.includes(key))) {
7
+ return result;
8
+ }
9
+
10
+ Object.keys(given).forEach(element => {
11
+ const verb = given[element];
12
+ if (!verb.responses) return;
13
+ if (options.parameters && !verb.parameters) return;
14
+ if (options.parameters && !verb.parameters.some(p => options.parameters.includes(p.in))) return;
15
+
16
+ if (Object.keys(verb.responses).some(response => options.responses.includes(response))) return;
17
+
18
+ const path = context.path || [];
19
+ result.push({
20
+ message: context.rule.message,
21
+ path: [...path, element]
22
+ });
23
+ });
24
+
25
+ return result;
26
+ };
@@ -0,0 +1,98 @@
1
+ // Check that format is valid for a schema type.
2
+
3
+ /**
4
+ * @param {object} given
5
+ * @param {object} options
6
+ * @param {object} options.formats
7
+ * @param {Array<string>} options.formats.string
8
+ * @param {Array<string>} options.formats.number
9
+ * @param {Array<string>} options.formats.integer
10
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
11
+ */
12
+ module.exports = function checkTypeAndFormat(given, options, context) {
13
+ if (given === null || typeof given !== "object") {
14
+ return [];
15
+ }
16
+
17
+ const errors = [];
18
+ const path = context.path || [];
19
+
20
+ if (given.type === "string" && options.formats.string) {
21
+ if (given.format) {
22
+ if (!options.formats.string.includes(given.format)) {
23
+ errors.push({
24
+ message: `Schema with type string has unrecognized format: ${given.format}`,
25
+ path: [...path, "format"]
26
+ });
27
+ }
28
+ }
29
+ } else if (given.type === "integer" && options.formats.integer) {
30
+ if (given.format) {
31
+ if (!options.formats.integer.includes(given.format)) {
32
+ errors.push({
33
+ message: `Schema with type integer has unrecognized format: ${given.format}`,
34
+ path: [...path, "format"]
35
+ });
36
+ }
37
+ } else {
38
+ errors.push({
39
+ message: "Schema with type integer should specify format",
40
+ path
41
+ });
42
+ }
43
+ } else if (given.type === "number" && options.formats.number) {
44
+ if (given.format) {
45
+ if (!options.formats.number.includes(given.format)) {
46
+ errors.push({
47
+ message: `Schema with type number has unrecognized format: ${given.format}`,
48
+ path: [...path, "format"]
49
+ });
50
+ }
51
+ } else {
52
+ errors.push({
53
+ message: "Schema with type number should specify format",
54
+ path
55
+ });
56
+ }
57
+ } else if (given.type === "boolean") {
58
+ if (given.format) {
59
+ errors.push({
60
+ message: "Schema with type boolean should not specify format",
61
+ path: [...path, "format"]
62
+ });
63
+ }
64
+ } else if (given.properties && typeof given.properties === "object") {
65
+ // eslint-disable-next-line no-restricted-syntax
66
+ for (const [key, value] of Object.entries(given.properties)) {
67
+ errors.push(...checkTypeAndFormat(value, options, {
68
+ path: [...path, "properties", key],
69
+ document: context.document,
70
+ documentInventory: context.documentInventory,
71
+ rule: context.rule
72
+ }));
73
+ }
74
+ }
75
+
76
+ if (given.type === "array") {
77
+ errors.push(...checkTypeAndFormat(given.items, options, {
78
+ path: [...path, "items"],
79
+ document: context.document,
80
+ documentInventory: context.documentInventory,
81
+ rule: context.rule
82
+ }));
83
+ }
84
+
85
+ if (given.allOf && Array.isArray(given.allOf)) {
86
+ // eslint-disable-next-line no-restricted-syntax
87
+ for (const [index, value] of given.allOf.entries()) {
88
+ errors.push(...checkTypeAndFormat(value, options, {
89
+ path: [...path, "allOf", index],
90
+ document: context.document,
91
+ documentInventory: context.documentInventory,
92
+ rule: context.rule
93
+ }));
94
+ }
95
+ }
96
+
97
+ return errors;
98
+ };
@@ -0,0 +1,76 @@
1
+ module.exports = function apqStandardResponseCodes(targetVal, options, context) {
2
+ const results = [];
3
+
4
+ if (!targetVal || !context?.path) return results;
5
+
6
+ const responses = targetVal.responses;
7
+ if (!responses || typeof responses !== 'object') return results;
8
+
9
+ const definedCodes = Object.keys(responses);
10
+ const pathIndex = context.path.indexOf('paths');
11
+ if (pathIndex === -1) return results;
12
+
13
+ const resourcePath = context.path[pathIndex + 1];
14
+ const verb = context.path[pathIndex + 2];
15
+
16
+ if (typeof resourcePath !== 'string' || typeof verb !== 'string') return results;
17
+
18
+ const exclusions = options?.['resources-exclusions'] || [];
19
+ if (exclusions.some(ex => {
20
+ const [exVerb, exPath] = ex.split(':');
21
+ return exVerb.toLowerCase() === verb.toLowerCase() && new RegExp(`^${exPath}$`).test(resourcePath);
22
+ })) return results;
23
+
24
+ const rulesConfig = options?.['required-codes-by-resources-paths'];
25
+ if (!rulesConfig) return results;
26
+
27
+ const rules = rulesConfig
28
+ .split(/[\n;]/)
29
+ .map(r => r.trim())
30
+ .filter(Boolean)
31
+ .map(rule => {
32
+ const parts = rule.split(':');
33
+ if (parts.length < 3) return null;
34
+
35
+ const verbPart = parts[0].toLowerCase();
36
+ const codesPart = parts[parts.length - 1];
37
+ const regexPart = parts.slice(1, -1).join(':');
38
+
39
+ return {
40
+ verb: verbPart,
41
+ pathRegex: new RegExp(regexPart),
42
+ requiredCodes: codesPart.split(',').map(c => c.trim()).filter(Boolean)
43
+ };
44
+ }).filter(Boolean);
45
+
46
+ const matchedRule = rules.find(
47
+ r => r.verb === verb.toLowerCase() && r.pathRegex.test(resourcePath)
48
+ );
49
+
50
+ if (!matchedRule) return results;
51
+
52
+ matchedRule.requiredCodes.forEach(ruleCode => {
53
+ let missing = false;
54
+ let msg = "";
55
+
56
+ if (ruleCode.includes('|')) {
57
+ const alternatives = ruleCode.split('|');
58
+ if (!alternatives.some(c => definedCodes.includes(c))) {
59
+ missing = true;
60
+ msg = `OAR039: Response code ${alternatives.join(' or ')} must be defined.`;
61
+ }
62
+ } else if (!definedCodes.includes(ruleCode)) {
63
+ missing = true;
64
+ msg = `OAR039: Response code ${ruleCode} must be defined.`;
65
+ }
66
+
67
+ if (missing) {
68
+ results.push({
69
+ message: msg,
70
+ path: context.path
71
+ });
72
+ }
73
+ });
74
+
75
+ return results;
76
+ };
@@ -0,0 +1,18 @@
1
+
2
+ /**
3
+ * @param {Object} given
4
+ * @param {object} options
5
+ * @param {string} options.property
6
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
7
+ */
8
+ module.exports = (given, options, context) => {
9
+ const errors = [];
10
+
11
+ if (!given || Object.keys(given).some(key => key.toUpperCase() !== options.property.toUpperCase())) {
12
+ errors.push({
13
+ message: context.rule.message
14
+ });
15
+ }
16
+
17
+ return errors;
18
+ };