UNPKG

@accordproject/concerto-linter

Version:

Concerto Linter using Spectral rulesets

174 lines (149 loc) 6.94 kB
/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Spectral, Document, IRuleResult, RulesetDefinition, Ruleset } from '@stoplight/spectral-core'; import { Json as JsonParsers } from '@stoplight/spectral-parsers'; import { resolveRulesetPath } from './config-loader'; import { getRuleset } from '@stoplight/spectral-cli/dist/services/linter/utils/getRuleset'; import concertoRuleset from '@accordproject/concerto-linter-default-ruleset'; import { Parser } from '@accordproject/concerto-cto'; interface options { /** Path to a custom Spectral ruleset or 'default' to use the built-in ruleset */ ruleset?: string; /** One or more namespaces to exclude from linting results */ excludeNamespaces?: string | string[]; } interface lintResult { /** Unique rule identifier (e.g. 'no-reserved-keywords') */ code: string; /** Human-readable description of the violation */ message: string; /** Severity level ('error' | 'warning' | 'info' | 'hint') */ severity: string; /** * JSONPath-style pointer as an array of keys/indices * (e.g. ['declarations', 3]) */ path: Array<string | number>; /** Namespace where the violation occurred */ namespace: string; } /** * Converts Concerto model to JSON AST representation * @param {string | object} model - Concerto model as string or parsed object * @returns {string} JSON string of the AST * @throws {Error} For invalid model inputs */ function convertToJsonAST(model: string | object): string { try { if (typeof model === 'string') { const modelFile = Parser.parseModels([model]); return JSON.stringify(modelFile); } return JSON.stringify(model); } catch (error) { throw new Error(`Model conversion failed: ${error instanceof Error ? error.message : error}`); } } /** * Loads Spectral ruleset based on configuration options * @param {string} [ruleset] - Custom ruleset path or 'default' * @returns {Promise<Ruleset | RulesetDefinition>} Loaded ruleset */ async function loadRuleset(ruleset?: string): Promise<Ruleset | RulesetDefinition> { try { if (typeof ruleset === 'string' && ruleset.toLowerCase() === 'default') { return concertoRuleset; } const rulesetPath = await resolveRulesetPath(ruleset); return rulesetPath ? await getRuleset(rulesetPath) : concertoRuleset; } catch (error) { throw new Error(`Ruleset loading failed: ${error instanceof Error ? error.message : error}`); } } /** * Formats Spectral linting results by mapping them to a standardized lint result structure, * extracting namespaces from the provided JSON AST, and filtering out results based on excluded namespaces. * * @param spectralResults - An array of Spectral rule results to be formatted. * @param jsonAST - A JSON string representing the AST, used to extract model namespaces. * @param excludeNamespaces - A string or array of strings specifying namespace patterns to exclude from the results. * Patterns ending with `.*` will match any namespace starting with the given prefix. * Defaults to `['concerto.*', 'org.accord.*']`. * @returns An array of formatted lint results, excluding those matching the specified namespaces. */ function formatResults( spectralResults: IRuleResult[], jsonAST: string, excludeNamespaces: string | string[] = ['concerto.*', 'org.accordproject.*'] ): lintResult[] { try { const ast = JSON.parse(jsonAST); const severityMap: { [key: number]: string } = { 0: 'error', 1: 'warning', 2: 'info', 3: 'hint', }; const results: lintResult[] = spectralResults.map(r => { let namespace = 'unknown'; if (Array.isArray(r.path) && r.path.length >= 2 && r.path[0] === 'models') { const modelIndex = r.path[1] as number; const modelEntry = ast.models?.[modelIndex]; if (modelEntry && modelEntry.namespace) { namespace = modelEntry.namespace; } } return { code: r.code as string, message: r.message, path: r.path, severity: severityMap[r.severity], namespace: namespace, }; }); const exclusionPatterns = Array.isArray(excludeNamespaces) ? excludeNamespaces : [excludeNamespaces]; return results.filter(result => { return !exclusionPatterns.some(pattern => { if (pattern.endsWith('.*')) { return result.namespace.startsWith(pattern.slice(0, pattern.length - 2)); } return result.namespace === pattern; }); }); } catch (error) { throw new Error(`Formatting lint results failed: ${error instanceof Error ? error.message : error}`); } } /** * Lints Concerto models using Spectral and Concerto rules. * @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. * @param {options} [config] - Configuration options for customizing the linting process. * @param {string} [config.ruleset] - Path to a custom Spectral ruleset file or 'default' to use the built-in ruleset. * @param {string | string[]} [config.excludeNamespaces] - One or more namespaces to exclude from linting results (defaults to 'concerto.*' and 'org.accord.*'). * @returns {Promise<lintResult[]>} Promise resolving to an array of formatted linting results as a JSON object. * @throws {Error} Throws an error if linting or model conversion fails. */ export async function lintModel(model: string | object, config?: options): Promise<lintResult[]> { try { const jsonAST = convertToJsonAST(model); const ruleset = await loadRuleset(config?.ruleset); const spectral = new Spectral(); spectral.setRuleset(ruleset); const document = new Document(jsonAST, JsonParsers); const spectralResults = await spectral.run(document); return formatResults(spectralResults, jsonAST, config?.excludeNamespaces); } catch (error) { throw new Error(`Linting process failed: ${error instanceof Error ? error.message : error}`); } }