@accordproject/concerto-linter 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.
- package/.eslintignore +20 -0
- package/.eslintrc.yml +45 -0
- package/HEADER.md +2 -0
- package/README.md +207 -0
- package/default-ruleset/.eslintignore +20 -0
- package/default-ruleset/.eslintrc.yml +45 -0
- package/default-ruleset/README.md +300 -0
- package/default-ruleset/jest.config.js +7 -0
- package/default-ruleset/package.json +51 -0
- package/default-ruleset/src/abstract-must-subclassed.ts +29 -0
- package/default-ruleset/src/camel-case-properties.ts +35 -0
- package/default-ruleset/src/functions/check-length-validator.ts +52 -0
- package/default-ruleset/src/functions/find-abstract-declaration.ts +70 -0
- package/default-ruleset/src/functions/find-empty-declarations.ts +56 -0
- package/default-ruleset/src/namespace-version.ts +35 -0
- package/default-ruleset/src/no-empty-declarations.ts +31 -0
- package/default-ruleset/src/no-reserved-keywords.ts +43 -0
- package/default-ruleset/src/pascal-case-declarations.ts +35 -0
- package/default-ruleset/src/pascal-case-decorators.ts +38 -0
- package/default-ruleset/src/ruleset-main.ts +41 -0
- package/default-ruleset/src/string-length-validator.ts +32 -0
- package/default-ruleset/src/upper-snake-case-enum-const.ts +36 -0
- package/default-ruleset/test/fixtures/ENUM_Constans-invaild.cto +7 -0
- package/default-ruleset/test/fixtures/ENUM_Constans-vaild.cto +7 -0
- package/default-ruleset/test/fixtures/abstract-must-subclassed-invalid.cto +10 -0
- package/default-ruleset/test/fixtures/abstract-must-subclassed-valid.cto +18 -0
- package/default-ruleset/test/fixtures/declarations-valid-PascalCase.cto +21 -0
- package/default-ruleset/test/fixtures/declarations-violate-PascalCase.cto +22 -0
- package/default-ruleset/test/fixtures/decorators-valid-PascalCase.cto +8 -0
- package/default-ruleset/test/fixtures/decorators-violate-PascalCase.cto +8 -0
- package/default-ruleset/test/fixtures/namespace-invalid-version.cto +5 -0
- package/default-ruleset/test/fixtures/namespace-valid-version.cto +5 -0
- package/default-ruleset/test/fixtures/no-empty-declarations-invalid.cto +10 -0
- package/default-ruleset/test/fixtures/no-empty-declarations-valid.cto +16 -0
- package/default-ruleset/test/fixtures/no-reserved-keywords-invalid.cto +16 -0
- package/default-ruleset/test/fixtures/no-reserved-keywords-valid.cto +16 -0
- package/default-ruleset/test/fixtures/properties-valid-camelCase.cto +10 -0
- package/default-ruleset/test/fixtures/properties-violate-camelCase.cto +10 -0
- package/default-ruleset/test/fixtures/string-length-validator-invalid.cto +9 -0
- package/default-ruleset/test/fixtures/string-length-validator-valid.cto +10 -0
- package/default-ruleset/test/rules/abstract-must-subclassed.test.ts +33 -0
- package/default-ruleset/test/rules/namespace-version.test.ts +24 -0
- package/default-ruleset/test/rules/naming-ruleset.test.ts +133 -0
- package/default-ruleset/test/rules/no-empty-declarations.test.ts +33 -0
- package/default-ruleset/test/rules/no-reserved-keywords.test.ts +33 -0
- package/default-ruleset/test/rules/string-length-validator.test.ts +33 -0
- package/default-ruleset/test/test-rule.ts +30 -0
- package/default-ruleset/tsconfig.json +113 -0
- package/dist/config-loader.d.ts +6 -0
- package/dist/config-loader.js +52 -0
- package/dist/config-loader.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +135 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +11 -0
- package/package.json +54 -0
- package/src/config-loader.ts +48 -0
- package/src/index.ts +173 -0
- package/test/unit/configLoader.test.ts +76 -0
- package/test/unit/formatResults.test.ts +149 -0
- package/test/unit/lintModel.test.ts +84 -0
- package/test/unit/loadRuleset.test.ts +64 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +113 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License.
|
|
5
|
+
* You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
* See the License for the specific language governing permissions and
|
|
13
|
+
* limitations under the License.
|
|
14
|
+
*/
|
|
15
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
16
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
17
|
+
};
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.resolveRulesetPath = resolveRulesetPath;
|
|
20
|
+
const find_up_1 = __importDefault(require("find-up"));
|
|
21
|
+
// Valid Spectral ruleset filenames in priority order
|
|
22
|
+
const SPECTRAL_RULESET_FILES = [
|
|
23
|
+
'.spectral.yaml',
|
|
24
|
+
'.spectral.yml',
|
|
25
|
+
'.spectral.json',
|
|
26
|
+
'.spectral.js',
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* Searches for a Spectral ruleset configuration file in the current and parent directories.
|
|
30
|
+
* @returns {Promise<string | null>} Path to found ruleset file or null if none exists
|
|
31
|
+
*/
|
|
32
|
+
async function findLocalRuleset() {
|
|
33
|
+
for (const fileName of SPECTRAL_RULESET_FILES) {
|
|
34
|
+
const foundPath = await (0, find_up_1.default)(fileName);
|
|
35
|
+
if (foundPath) {
|
|
36
|
+
return foundPath;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolves the Spectral ruleset location based on user input or directory search
|
|
43
|
+
* @param {string} [customPath] User-provided ruleset path
|
|
44
|
+
* @returns {Promise<string | null>} Path to custom ruleset, null for default ruleset
|
|
45
|
+
*/
|
|
46
|
+
async function resolveRulesetPath(customPath) {
|
|
47
|
+
if (!customPath) {
|
|
48
|
+
return await findLocalRuleset();
|
|
49
|
+
}
|
|
50
|
+
return customPath;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=config-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-loader.js","sourceRoot":"","sources":["../src/config-loader.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;;;;AA6BH,gDAMC;AAjCD,sDAA6B;AAE7B,qDAAqD;AACrD,MAAM,sBAAsB,GAAG;IAC3B,gBAAgB;IAChB,eAAe;IACf,gBAAgB;IAChB,cAAc;CACjB,CAAC;AAEF;;;GAGG;AACH,KAAK,UAAU,gBAAgB;IAC3B,KAAK,MAAM,QAAQ,IAAI,sBAAsB,EAAE,CAAC;QAC5C,MAAM,SAAS,GAAG,MAAM,IAAA,iBAAM,EAAC,QAAQ,CAAC,CAAC;QACzC,IAAI,SAAS,EAAE,CAAC;YAAA,OAAO,SAAS,CAAC;QAAA,CAAC;IACtC,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACI,KAAK,UAAU,kBAAkB,CAAC,UAAmB;IACxD,IAAI,CAAC,UAAU,EAAE,CAAC;QACd,OAAO,MAAM,gBAAgB,EAAE,CAAC;IACpC,CAAC;IAED,OAAO,UAAU,CAAC;AACtB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
interface options {
|
|
2
|
+
/** Path to a custom Spectral ruleset or 'default' to use the built-in ruleset */
|
|
3
|
+
ruleset?: string;
|
|
4
|
+
/** One or more namespaces to exclude from linting results */
|
|
5
|
+
excludeNamespaces?: string | string[];
|
|
6
|
+
}
|
|
7
|
+
interface lintResult {
|
|
8
|
+
/** Unique rule identifier (e.g. 'no-reserved-keywords') */
|
|
9
|
+
code: string;
|
|
10
|
+
/** Human-readable description of the violation */
|
|
11
|
+
message: string;
|
|
12
|
+
/** Severity level ('error' | 'warning' | 'info' | 'hint') */
|
|
13
|
+
severity: string;
|
|
14
|
+
/**
|
|
15
|
+
* JSONPath-style pointer as an array of keys/indices
|
|
16
|
+
* (e.g. ['declarations', 3])
|
|
17
|
+
*/
|
|
18
|
+
path: Array<string | number>;
|
|
19
|
+
/** Namespace where the violation occurred */
|
|
20
|
+
namespace: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Lints Concerto models using Spectral and Concerto rules.
|
|
24
|
+
* @param {string | object} model - The Concerto model to lint, either as a CTO string or a parsed AST object. Note: No external dependency resolution is performed.
|
|
25
|
+
* @param {options} [config] - Configuration options for customizing the linting process.
|
|
26
|
+
* @param {string} [config.ruleset] - Path to a custom Spectral ruleset file or 'default' to use the built-in ruleset.
|
|
27
|
+
* @param {string | string[]} [config.excludeNamespaces] - One or more namespaces to exclude from linting results (defaults to 'concerto.*' and 'org.accord.*').
|
|
28
|
+
* @returns {Promise<lintResult[]>} Promise resolving to an array of formatted linting results as a JSON object.
|
|
29
|
+
* @throws {Error} Throws an error if linting or model conversion fails.
|
|
30
|
+
*/
|
|
31
|
+
export declare function lintModel(model: string | object, config?: options): Promise<lintResult[]>;
|
|
32
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License.
|
|
5
|
+
* You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
* See the License for the specific language governing permissions and
|
|
13
|
+
* limitations under the License.
|
|
14
|
+
*/
|
|
15
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
16
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
17
|
+
};
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.lintModel = lintModel;
|
|
20
|
+
const spectral_core_1 = require("@stoplight/spectral-core");
|
|
21
|
+
const spectral_parsers_1 = require("@stoplight/spectral-parsers");
|
|
22
|
+
const config_loader_1 = require("./config-loader");
|
|
23
|
+
const getRuleset_1 = require("@stoplight/spectral-cli/dist/services/linter/utils/getRuleset");
|
|
24
|
+
const concerto_linter_default_ruleset_1 = __importDefault(require("@accordproject/concerto-linter-default-ruleset"));
|
|
25
|
+
const concerto_cto_1 = require("@accordproject/concerto-cto");
|
|
26
|
+
/**
|
|
27
|
+
* Converts Concerto model to JSON AST representation
|
|
28
|
+
* @param {string | object} model - Concerto model as string or parsed object
|
|
29
|
+
* @returns {string} JSON string of the AST
|
|
30
|
+
* @throws {Error} For invalid model inputs
|
|
31
|
+
*/
|
|
32
|
+
function convertToJsonAST(model) {
|
|
33
|
+
try {
|
|
34
|
+
if (typeof model === 'string') {
|
|
35
|
+
const modelFile = concerto_cto_1.Parser.parseModels([model]);
|
|
36
|
+
return JSON.stringify(modelFile);
|
|
37
|
+
}
|
|
38
|
+
return JSON.stringify(model);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new Error(`Model conversion failed: ${error instanceof Error ? error.message : error}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Loads Spectral ruleset based on configuration options
|
|
46
|
+
* @param {string} [ruleset] - Custom ruleset path or 'default'
|
|
47
|
+
* @returns {Promise<Ruleset | RulesetDefinition>} Loaded ruleset
|
|
48
|
+
*/
|
|
49
|
+
async function loadRuleset(ruleset) {
|
|
50
|
+
try {
|
|
51
|
+
if (typeof ruleset === 'string' && ruleset.toLowerCase() === 'default') {
|
|
52
|
+
return concerto_linter_default_ruleset_1.default;
|
|
53
|
+
}
|
|
54
|
+
const rulesetPath = await (0, config_loader_1.resolveRulesetPath)(ruleset);
|
|
55
|
+
return rulesetPath ? await (0, getRuleset_1.getRuleset)(rulesetPath) : concerto_linter_default_ruleset_1.default;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw new Error(`Ruleset loading failed: ${error instanceof Error ? error.message : error}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Formats Spectral linting results by mapping them to a standardized lint result structure,
|
|
63
|
+
* extracting namespaces from the provided JSON AST, and filtering out results based on excluded namespaces.
|
|
64
|
+
*
|
|
65
|
+
* @param spectralResults - An array of Spectral rule results to be formatted.
|
|
66
|
+
* @param jsonAST - A JSON string representing the AST, used to extract model namespaces.
|
|
67
|
+
* @param excludeNamespaces - A string or array of strings specifying namespace patterns to exclude from the results.
|
|
68
|
+
* Patterns ending with `.*` will match any namespace starting with the given prefix.
|
|
69
|
+
* Defaults to `['concerto.*', 'org.accord.*']`.
|
|
70
|
+
* @returns An array of formatted lint results, excluding those matching the specified namespaces.
|
|
71
|
+
*/
|
|
72
|
+
function formatResults(spectralResults, jsonAST, excludeNamespaces = ['concerto.*', 'org.accordproject.*']) {
|
|
73
|
+
try {
|
|
74
|
+
const ast = JSON.parse(jsonAST);
|
|
75
|
+
const severityMap = {
|
|
76
|
+
0: 'error',
|
|
77
|
+
1: 'warning',
|
|
78
|
+
2: 'info',
|
|
79
|
+
3: 'hint',
|
|
80
|
+
};
|
|
81
|
+
const results = spectralResults.map(r => {
|
|
82
|
+
let namespace = 'unknown';
|
|
83
|
+
if (Array.isArray(r.path) && r.path.length >= 2 && r.path[0] === 'models') {
|
|
84
|
+
const modelIndex = r.path[1];
|
|
85
|
+
const modelEntry = ast.models?.[modelIndex];
|
|
86
|
+
if (modelEntry && modelEntry.namespace) {
|
|
87
|
+
namespace = modelEntry.namespace;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
code: r.code,
|
|
92
|
+
message: r.message,
|
|
93
|
+
path: r.path,
|
|
94
|
+
severity: severityMap[r.severity],
|
|
95
|
+
namespace: namespace,
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
const exclusionPatterns = Array.isArray(excludeNamespaces) ? excludeNamespaces : [excludeNamespaces];
|
|
99
|
+
return results.filter(result => {
|
|
100
|
+
return !exclusionPatterns.some(pattern => {
|
|
101
|
+
if (pattern.endsWith('.*')) {
|
|
102
|
+
return result.namespace.startsWith(pattern.slice(0, pattern.length - 2));
|
|
103
|
+
}
|
|
104
|
+
return result.namespace === pattern;
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
throw new Error(`Formatting lint results failed: ${error instanceof Error ? error.message : error}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Lints Concerto models using Spectral and Concerto rules.
|
|
114
|
+
* @param {string | object} model - The Concerto model to lint, either as a CTO string or a parsed AST object. Note: No external dependency resolution is performed.
|
|
115
|
+
* @param {options} [config] - Configuration options for customizing the linting process.
|
|
116
|
+
* @param {string} [config.ruleset] - Path to a custom Spectral ruleset file or 'default' to use the built-in ruleset.
|
|
117
|
+
* @param {string | string[]} [config.excludeNamespaces] - One or more namespaces to exclude from linting results (defaults to 'concerto.*' and 'org.accord.*').
|
|
118
|
+
* @returns {Promise<lintResult[]>} Promise resolving to an array of formatted linting results as a JSON object.
|
|
119
|
+
* @throws {Error} Throws an error if linting or model conversion fails.
|
|
120
|
+
*/
|
|
121
|
+
async function lintModel(model, config) {
|
|
122
|
+
try {
|
|
123
|
+
const jsonAST = convertToJsonAST(model);
|
|
124
|
+
const ruleset = await loadRuleset(config?.ruleset);
|
|
125
|
+
const spectral = new spectral_core_1.Spectral();
|
|
126
|
+
spectral.setRuleset(ruleset);
|
|
127
|
+
const document = new spectral_core_1.Document(jsonAST, spectral_parsers_1.Json);
|
|
128
|
+
const spectralResults = await spectral.run(document);
|
|
129
|
+
return formatResults(spectralResults, jsonAST, config?.excludeNamespaces);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
throw new Error(`Linting process failed: ${error instanceof Error ? error.message : error}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@accordproject/concerto-linter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Concerto Linter using Spectral rulesets",
|
|
5
|
+
"homepage": "https://github.com/accordproject/concerto",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18",
|
|
8
|
+
"npm": ">=10"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"typings": "dist/index.d.ts",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"clean": "rimraf dist",
|
|
14
|
+
"prebuild": "npm-run-all clean && cd ./default-ruleset && npm run build",
|
|
15
|
+
"build": "tsc -p tsconfig.build.json",
|
|
16
|
+
"pretest": "npm-run-all lint",
|
|
17
|
+
"lint": "eslint .",
|
|
18
|
+
"lint:fix": "eslint . --fix",
|
|
19
|
+
"test": "jest",
|
|
20
|
+
"test:watch": "jest --watchAll"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/accordproject/concerto.git",
|
|
25
|
+
"directory": "packages/concerto-linter"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"concerto",
|
|
29
|
+
"linter",
|
|
30
|
+
"spectral"
|
|
31
|
+
],
|
|
32
|
+
"author": "accordproject.org",
|
|
33
|
+
"license": "Apache-2.0",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"ajv": "^8.17.1",
|
|
36
|
+
"@accordproject/concerto-core": "^4.0.0-alpha.1",
|
|
37
|
+
"@accordproject/concerto-linter-default-ruleset": "file:./default-ruleset",
|
|
38
|
+
"@accordproject/concerto-metamodel": "^4.0.0-alpha.1",
|
|
39
|
+
"find-up": "5.0.0",
|
|
40
|
+
"@stoplight/spectral-cli": "6.15.0",
|
|
41
|
+
"@stoplight/spectral-core": "1.20.0",
|
|
42
|
+
"@stoplight/spectral-parsers": "1.0.5"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"eslint": "8.57.1",
|
|
46
|
+
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
|
47
|
+
"@typescript-eslint/parser": "^8.46.2",
|
|
48
|
+
"npm-run-all": "^4.1.5",
|
|
49
|
+
"rimraf": "^6.0.1",
|
|
50
|
+
"typescript": "5.7.2",
|
|
51
|
+
"jest": "^29.7.0",
|
|
52
|
+
"ts-jest": "^29.2.5"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
* you may not use this file except in compliance with the License.
|
|
4
|
+
* You may obtain a copy of the License at
|
|
5
|
+
*
|
|
6
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
*
|
|
8
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
* See the License for the specific language governing permissions and
|
|
12
|
+
* limitations under the License.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import findUp from 'find-up';
|
|
16
|
+
|
|
17
|
+
// Valid Spectral ruleset filenames in priority order
|
|
18
|
+
const SPECTRAL_RULESET_FILES = [
|
|
19
|
+
'.spectral.yaml',
|
|
20
|
+
'.spectral.yml',
|
|
21
|
+
'.spectral.json',
|
|
22
|
+
'.spectral.js',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Searches for a Spectral ruleset configuration file in the current and parent directories.
|
|
27
|
+
* @returns {Promise<string | null>} Path to found ruleset file or null if none exists
|
|
28
|
+
*/
|
|
29
|
+
async function findLocalRuleset(): Promise<string | null> {
|
|
30
|
+
for (const fileName of SPECTRAL_RULESET_FILES) {
|
|
31
|
+
const foundPath = await findUp(fileName);
|
|
32
|
+
if (foundPath) {return foundPath;}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolves the Spectral ruleset location based on user input or directory search
|
|
39
|
+
* @param {string} [customPath] User-provided ruleset path
|
|
40
|
+
* @returns {Promise<string | null>} Path to custom ruleset, null for default ruleset
|
|
41
|
+
*/
|
|
42
|
+
export async function resolveRulesetPath(customPath?: string): Promise<string | null> {
|
|
43
|
+
if (!customPath) {
|
|
44
|
+
return await findLocalRuleset();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return customPath;
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
* you may not use this file except in compliance with the License.
|
|
4
|
+
* You may obtain a copy of the License at
|
|
5
|
+
*
|
|
6
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
*
|
|
8
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
* See the License for the specific language governing permissions and
|
|
12
|
+
* limitations under the License.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Spectral, Document, IRuleResult, RulesetDefinition, Ruleset } from '@stoplight/spectral-core';
|
|
16
|
+
import { Json as JsonParsers } from '@stoplight/spectral-parsers';
|
|
17
|
+
import { resolveRulesetPath } from './config-loader';
|
|
18
|
+
import { getRuleset } from '@stoplight/spectral-cli/dist/services/linter/utils/getRuleset';
|
|
19
|
+
import concertoRuleset from '@accordproject/concerto-linter-default-ruleset';
|
|
20
|
+
import { Parser } from '@accordproject/concerto-cto';
|
|
21
|
+
|
|
22
|
+
interface options {
|
|
23
|
+
/** Path to a custom Spectral ruleset or 'default' to use the built-in ruleset */
|
|
24
|
+
ruleset?: string;
|
|
25
|
+
|
|
26
|
+
/** One or more namespaces to exclude from linting results */
|
|
27
|
+
excludeNamespaces?: string | string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface lintResult {
|
|
31
|
+
/** Unique rule identifier (e.g. 'no-reserved-keywords') */
|
|
32
|
+
code: string;
|
|
33
|
+
|
|
34
|
+
/** Human-readable description of the violation */
|
|
35
|
+
message: string;
|
|
36
|
+
|
|
37
|
+
/** Severity level ('error' | 'warning' | 'info' | 'hint') */
|
|
38
|
+
severity: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* JSONPath-style pointer as an array of keys/indices
|
|
42
|
+
* (e.g. ['declarations', 3])
|
|
43
|
+
*/
|
|
44
|
+
path: Array<string | number>;
|
|
45
|
+
|
|
46
|
+
/** Namespace where the violation occurred */
|
|
47
|
+
namespace: string;
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Converts Concerto model to JSON AST representation
|
|
53
|
+
* @param {string | object} model - Concerto model as string or parsed object
|
|
54
|
+
* @returns {string} JSON string of the AST
|
|
55
|
+
* @throws {Error} For invalid model inputs
|
|
56
|
+
*/
|
|
57
|
+
function convertToJsonAST(model: string | object): string {
|
|
58
|
+
try {
|
|
59
|
+
if (typeof model === 'string') {
|
|
60
|
+
const modelFile = Parser.parseModels([model]);
|
|
61
|
+
return JSON.stringify(modelFile);
|
|
62
|
+
}
|
|
63
|
+
return JSON.stringify(model);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error(`Model conversion failed: ${error instanceof Error ? error.message : error}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Loads Spectral ruleset based on configuration options
|
|
71
|
+
* @param {string} [ruleset] - Custom ruleset path or 'default'
|
|
72
|
+
* @returns {Promise<Ruleset | RulesetDefinition>} Loaded ruleset
|
|
73
|
+
*/
|
|
74
|
+
async function loadRuleset(ruleset?: string): Promise<Ruleset | RulesetDefinition> {
|
|
75
|
+
try {
|
|
76
|
+
|
|
77
|
+
if (typeof ruleset === 'string' && ruleset.toLowerCase() === 'default') {
|
|
78
|
+
return concertoRuleset;
|
|
79
|
+
}
|
|
80
|
+
const rulesetPath = await resolveRulesetPath(ruleset);
|
|
81
|
+
return rulesetPath ? await getRuleset(rulesetPath) : concertoRuleset;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw new Error(`Ruleset loading failed: ${error instanceof Error ? error.message : error}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Formats Spectral linting results by mapping them to a standardized lint result structure,
|
|
89
|
+
* extracting namespaces from the provided JSON AST, and filtering out results based on excluded namespaces.
|
|
90
|
+
*
|
|
91
|
+
* @param spectralResults - An array of Spectral rule results to be formatted.
|
|
92
|
+
* @param jsonAST - A JSON string representing the AST, used to extract model namespaces.
|
|
93
|
+
* @param excludeNamespaces - A string or array of strings specifying namespace patterns to exclude from the results.
|
|
94
|
+
* Patterns ending with `.*` will match any namespace starting with the given prefix.
|
|
95
|
+
* Defaults to `['concerto.*', 'org.accord.*']`.
|
|
96
|
+
* @returns An array of formatted lint results, excluding those matching the specified namespaces.
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
function formatResults(
|
|
100
|
+
spectralResults: IRuleResult[],
|
|
101
|
+
jsonAST: string,
|
|
102
|
+
excludeNamespaces: string | string[] = ['concerto.*', 'org.accordproject.*']
|
|
103
|
+
): lintResult[] {
|
|
104
|
+
try {
|
|
105
|
+
const ast = JSON.parse(jsonAST);
|
|
106
|
+
|
|
107
|
+
const severityMap: { [key: number]: string } = {
|
|
108
|
+
0: 'error',
|
|
109
|
+
1: 'warning',
|
|
110
|
+
2: 'info',
|
|
111
|
+
3: 'hint',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const results: lintResult[] = spectralResults.map(r => {
|
|
115
|
+
let namespace = 'unknown';
|
|
116
|
+
|
|
117
|
+
if (Array.isArray(r.path) && r.path.length >= 2 && r.path[0] === 'models') {
|
|
118
|
+
const modelIndex = r.path[1] as number;
|
|
119
|
+
const modelEntry = ast.models?.[modelIndex];
|
|
120
|
+
if (modelEntry && modelEntry.namespace) {
|
|
121
|
+
namespace = modelEntry.namespace;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
code: r.code as string,
|
|
127
|
+
message: r.message,
|
|
128
|
+
path: r.path,
|
|
129
|
+
severity: severityMap[r.severity],
|
|
130
|
+
namespace: namespace,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const exclusionPatterns = Array.isArray(excludeNamespaces) ? excludeNamespaces : [excludeNamespaces];
|
|
135
|
+
|
|
136
|
+
return results.filter(result => {
|
|
137
|
+
return !exclusionPatterns.some(pattern => {
|
|
138
|
+
if (pattern.endsWith('.*')) {
|
|
139
|
+
return result.namespace.startsWith(pattern.slice(0, pattern.length - 2));
|
|
140
|
+
}
|
|
141
|
+
return result.namespace === pattern;
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw new Error(`Formatting lint results failed: ${error instanceof Error ? error.message : error}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Lints Concerto models using Spectral and Concerto rules.
|
|
151
|
+
* @param {string | object} model - The Concerto model to lint, either as a CTO string or a parsed AST object. Note: No external dependency resolution is performed.
|
|
152
|
+
* @param {options} [config] - Configuration options for customizing the linting process.
|
|
153
|
+
* @param {string} [config.ruleset] - Path to a custom Spectral ruleset file or 'default' to use the built-in ruleset.
|
|
154
|
+
* @param {string | string[]} [config.excludeNamespaces] - One or more namespaces to exclude from linting results (defaults to 'concerto.*' and 'org.accord.*').
|
|
155
|
+
* @returns {Promise<lintResult[]>} Promise resolving to an array of formatted linting results as a JSON object.
|
|
156
|
+
* @throws {Error} Throws an error if linting or model conversion fails.
|
|
157
|
+
*/
|
|
158
|
+
export async function lintModel(model: string | object, config?: options): Promise<lintResult[]> {
|
|
159
|
+
try {
|
|
160
|
+
const jsonAST = convertToJsonAST(model);
|
|
161
|
+
const ruleset = await loadRuleset(config?.ruleset);
|
|
162
|
+
|
|
163
|
+
const spectral = new Spectral();
|
|
164
|
+
spectral.setRuleset(ruleset);
|
|
165
|
+
|
|
166
|
+
const document = new Document(jsonAST, JsonParsers);
|
|
167
|
+
const spectralResults = await spectral.run(document);
|
|
168
|
+
return formatResults(spectralResults, jsonAST, config?.excludeNamespaces);
|
|
169
|
+
|
|
170
|
+
} catch (error) {
|
|
171
|
+
throw new Error(`Linting process failed: ${error instanceof Error ? error.message : error}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { resolveRulesetPath } from '../../src/config-loader';
|
|
2
|
+
import findUp from 'find-up';
|
|
3
|
+
|
|
4
|
+
// Mock the find-up module
|
|
5
|
+
jest.mock('find-up');
|
|
6
|
+
const mockFindUp = findUp as jest.MockedFunction<typeof findUp>;
|
|
7
|
+
|
|
8
|
+
describe('resolveRulesetPath', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
mockFindUp.mockReset();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
it('should return the provided path when rulesetOption is a custom path', async () => {
|
|
16
|
+
const customPath = '/path/to/custom/ruleset.yaml';
|
|
17
|
+
const result = await resolveRulesetPath(customPath);
|
|
18
|
+
expect(result).toBe(customPath);
|
|
19
|
+
expect(mockFindUp).not.toHaveBeenCalled();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should search for local ruleset when no rulesetOption is provided', async () => {
|
|
23
|
+
const foundPath = '/project/.spectral.yaml';
|
|
24
|
+
mockFindUp
|
|
25
|
+
.mockResolvedValueOnce(foundPath) // First file found
|
|
26
|
+
.mockResolvedValueOnce(undefined) // Subsequent calls return undefined
|
|
27
|
+
.mockResolvedValueOnce(undefined)
|
|
28
|
+
.mockResolvedValueOnce(undefined);
|
|
29
|
+
|
|
30
|
+
const result = await resolveRulesetPath();
|
|
31
|
+
|
|
32
|
+
expect(result).toBe(foundPath);
|
|
33
|
+
expect(mockFindUp).toHaveBeenCalledWith('.spectral.yaml');
|
|
34
|
+
expect(mockFindUp).toHaveBeenCalledTimes(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should try all ruleset files in order when searching locally', async () => {
|
|
38
|
+
const foundPath = '/project/.spectral.json';
|
|
39
|
+
mockFindUp
|
|
40
|
+
.mockResolvedValueOnce(undefined) // .spectral.yaml not found
|
|
41
|
+
.mockResolvedValueOnce(undefined) // .spectral.yml not found
|
|
42
|
+
.mockResolvedValueOnce(foundPath) // .spectral.json found
|
|
43
|
+
.mockResolvedValueOnce(undefined); // .spectral.js not called
|
|
44
|
+
|
|
45
|
+
const result = await resolveRulesetPath();
|
|
46
|
+
|
|
47
|
+
expect(result).toBe(foundPath);
|
|
48
|
+
expect(mockFindUp).toHaveBeenCalledWith('.spectral.yaml');
|
|
49
|
+
expect(mockFindUp).toHaveBeenCalledWith('.spectral.yml');
|
|
50
|
+
expect(mockFindUp).toHaveBeenCalledWith('.spectral.json');
|
|
51
|
+
expect(mockFindUp).toHaveBeenCalledTimes(3);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return null when no local ruleset files are found', async () => {
|
|
55
|
+
mockFindUp.mockResolvedValue(undefined);
|
|
56
|
+
|
|
57
|
+
const result = await resolveRulesetPath();
|
|
58
|
+
|
|
59
|
+
expect(result).toBeNull();
|
|
60
|
+
expect(mockFindUp).toHaveBeenCalledTimes(4); // All 4 files checked
|
|
61
|
+
expect(mockFindUp).toHaveBeenCalledWith('.spectral.yaml');
|
|
62
|
+
expect(mockFindUp).toHaveBeenCalledWith('.spectral.yml');
|
|
63
|
+
expect(mockFindUp).toHaveBeenCalledWith('.spectral.json');
|
|
64
|
+
expect(mockFindUp).toHaveBeenCalledWith('.spectral.js');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should handle empty string as rulesetOption', async () => {
|
|
68
|
+
const foundPath = '/project/.spectral.yaml';
|
|
69
|
+
mockFindUp.mockResolvedValueOnce(foundPath);
|
|
70
|
+
|
|
71
|
+
const result = await resolveRulesetPath('');
|
|
72
|
+
|
|
73
|
+
expect(result).toBe(foundPath);
|
|
74
|
+
expect(mockFindUp).toHaveBeenCalledWith('.spectral.yaml');
|
|
75
|
+
});
|
|
76
|
+
});
|