@umbrelladocs/rdformat-validator
Version:
Validate and fix Reviewdog Diagnostic Format (RD Format) - A comprehensive library and CLI tool for validating JSON data against the Reviewdog Diagnostic Format specification
779 lines • 37.8 kB
JavaScript
"use strict";
/**
* Core validator module for RDFormat validation
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Validator = exports.ErrorReporter = exports.ValidationErrorCode = void 0;
exports.validate = validate;
exports.validateField = validateField;
const schema_1 = require("../types/schema");
/**
* Error codes for different types of validation failures
*/
var ValidationErrorCode;
(function (ValidationErrorCode) {
// Input validation errors
ValidationErrorCode["NULL_INPUT"] = "NULL_INPUT";
ValidationErrorCode["EMPTY_INPUT"] = "EMPTY_INPUT";
ValidationErrorCode["INVALID_JSON"] = "INVALID_JSON";
// Type validation errors
ValidationErrorCode["TYPE_MISMATCH"] = "TYPE_MISMATCH";
// Schema validation errors
ValidationErrorCode["ONEOF_VALIDATION_FAILED"] = "ONEOF_VALIDATION_FAILED";
ValidationErrorCode["ENUM_VALIDATION_FAILED"] = "ENUM_VALIDATION_FAILED";
// String validation errors
ValidationErrorCode["MIN_LENGTH_VIOLATION"] = "MIN_LENGTH_VIOLATION";
ValidationErrorCode["MAX_LENGTH_VIOLATION"] = "MAX_LENGTH_VIOLATION";
ValidationErrorCode["PATTERN_MISMATCH"] = "PATTERN_MISMATCH";
ValidationErrorCode["EMPTY_STRING"] = "EMPTY_STRING";
// Number validation errors
ValidationErrorCode["MIN_VALUE_VIOLATION"] = "MIN_VALUE_VIOLATION";
ValidationErrorCode["MAX_VALUE_VIOLATION"] = "MAX_VALUE_VIOLATION";
ValidationErrorCode["INVALID_NUMBER"] = "INVALID_NUMBER";
// Object validation errors
ValidationErrorCode["REQUIRED_PROPERTY_MISSING"] = "REQUIRED_PROPERTY_MISSING";
ValidationErrorCode["UNKNOWN_PROPERTY"] = "UNKNOWN_PROPERTY";
ValidationErrorCode["INVALID_OBJECT_STRUCTURE"] = "INVALID_OBJECT_STRUCTURE";
// Array validation errors
ValidationErrorCode["INVALID_ARRAY_ITEM"] = "INVALID_ARRAY_ITEM";
ValidationErrorCode["EMPTY_ARRAY"] = "EMPTY_ARRAY";
// RDFormat specific errors
ValidationErrorCode["INVALID_SEVERITY"] = "INVALID_SEVERITY";
ValidationErrorCode["INVALID_LOCATION"] = "INVALID_LOCATION";
ValidationErrorCode["INVALID_RANGE"] = "INVALID_RANGE";
ValidationErrorCode["INVALID_POSITION"] = "INVALID_POSITION";
ValidationErrorCode["MISSING_DIAGNOSTIC_MESSAGE"] = "MISSING_DIAGNOSTIC_MESSAGE";
ValidationErrorCode["MISSING_DIAGNOSTIC_LOCATION"] = "MISSING_DIAGNOSTIC_LOCATION";
})(ValidationErrorCode || (exports.ValidationErrorCode = ValidationErrorCode = {}));
/**
* Enhanced error reporter for detailed validation feedback
*/
class ErrorReporter {
constructor(strictMode = false) {
this.strictMode = strictMode;
}
/**
* Creates a detailed validation error with context
*/
createError(path, code, value, context) {
const error = {
path,
code,
value,
message: this.getErrorMessage(code, path, value, context),
expected: context?.expected
};
return error;
}
/**
* Creates a validation warning
*/
createWarning(path, code, message) {
return {
path,
code,
message: message || this.getWarningMessage(code, path)
};
}
/**
* Gets a human-readable error message based on the error code
*/
getErrorMessage(code, path, value, context) {
const pathDisplay = path ? ` at '${path}'` : '';
switch (code) {
case ValidationErrorCode.NULL_INPUT:
return 'Input cannot be null or undefined. Please provide valid RDFormat data.';
case ValidationErrorCode.EMPTY_INPUT:
return 'Input cannot be empty. Please provide valid RDFormat data.';
case ValidationErrorCode.TYPE_MISMATCH:
return `Expected ${context?.expected || 'different type'}${pathDisplay}, but got ${typeof value}. ${context?.suggestion || ''}`;
case ValidationErrorCode.REQUIRED_PROPERTY_MISSING:
const propName = path.split('.').pop() || path.split('[').pop()?.replace(']', '');
return `Missing required property '${propName}'${pathDisplay}. This field is mandatory in RDFormat.`;
case ValidationErrorCode.UNKNOWN_PROPERTY:
const unknownProp = path.split('.').pop() || path.split('[').pop()?.replace(']', '');
return `Unknown property '${unknownProp}'${pathDisplay}. This property is not part of the RDFormat specification.`;
case ValidationErrorCode.ENUM_VALIDATION_FAILED:
return `Invalid value '${value}'${pathDisplay}. ${context?.expected || 'Must be one of the allowed values'}.`;
case ValidationErrorCode.MIN_LENGTH_VIOLATION:
return `String${pathDisplay} must be at least ${context?.constraint} characters long, but got ${typeof value === 'string' ? value.length : 0} characters.`;
case ValidationErrorCode.MAX_LENGTH_VIOLATION:
return `String${pathDisplay} must be at most ${context?.constraint} characters long, but got ${typeof value === 'string' ? value.length : 0} characters.`;
case ValidationErrorCode.PATTERN_MISMATCH:
return `String${pathDisplay} does not match the required format. ${context?.expected || 'Please check the pattern requirements'}.`;
case ValidationErrorCode.EMPTY_STRING:
return `String${pathDisplay} cannot be empty. Please provide a non-empty value.`;
case ValidationErrorCode.MIN_VALUE_VIOLATION:
return `Number${pathDisplay} must be at least ${context?.constraint}, but got ${value}.`;
case ValidationErrorCode.MAX_VALUE_VIOLATION:
return `Number${pathDisplay} must be at most ${context?.constraint}, but got ${value}.`;
case ValidationErrorCode.ONEOF_VALIDATION_FAILED:
return `Value${pathDisplay} does not match any of the expected RDFormat structures. Please ensure your data follows one of the supported formats (single diagnostic, array of diagnostics, or diagnostic result).`;
case ValidationErrorCode.MISSING_DIAGNOSTIC_MESSAGE:
return `Diagnostic${pathDisplay} is missing the required 'message' field. Every diagnostic must have a descriptive message.`;
case ValidationErrorCode.MISSING_DIAGNOSTIC_LOCATION:
return `Diagnostic${pathDisplay} is missing the required 'location' field. Every diagnostic must specify where the issue was found.`;
case ValidationErrorCode.INVALID_LOCATION:
return `Location${pathDisplay} is invalid. A location must have a 'path' field and optionally a 'range' field.`;
case ValidationErrorCode.INVALID_RANGE:
return `Range${pathDisplay} is invalid. A range must have a 'start' position and optionally an 'end' position.`;
case ValidationErrorCode.INVALID_POSITION:
return `Position${pathDisplay} is invalid. A position must have a positive integer (1-based line number) and optionally a column number.`;
case ValidationErrorCode.INVALID_SEVERITY:
return `Severity${pathDisplay} must be one of: UNKNOWN_SEVERITY, ERROR, WARNING, INFO. Got '${value}'.`;
default:
return `Validation failed${pathDisplay}: ${context?.expected || 'Invalid value'}.`;
}
}
/**
* Gets a human-readable warning message
*/
getWarningMessage(code, path) {
const pathDisplay = path ? ` at '${path}'` : '';
switch (code) {
case ValidationErrorCode.UNKNOWN_PROPERTY:
const propName = path.split('.').pop() || path.split('[').pop()?.replace(']', '');
return `Property '${propName}'${pathDisplay} is not part of the RDFormat specification but will be ignored.`;
default:
return `Warning${pathDisplay}: Potential issue detected.`;
}
}
}
exports.ErrorReporter = ErrorReporter;
class Validator {
constructor(options = {}) {
this.options = {
strictMode: false,
allowExtraFields: true,
...options
};
this.schema = schema_1.rdformatSchema;
this.errorReporter = new ErrorReporter(this.options.strictMode || false);
}
/**
* Validates data against the RDFormat schema
*/
validate(data) {
const errors = [];
const warnings = [];
// Handle null or undefined input
if (data === null || data === undefined) {
errors.push(this.errorReporter.createError('', ValidationErrorCode.NULL_INPUT, data, {
expected: 'valid RDFormat data'
}));
return { valid: false, errors, warnings };
}
// Handle empty input
if (typeof data === 'string' && data.trim() === '') {
errors.push(this.errorReporter.createError('', ValidationErrorCode.EMPTY_INPUT, data, {
expected: 'non-empty RDFormat data'
}));
return { valid: false, errors, warnings };
}
// Handle empty objects
if (typeof data === 'object' && !Array.isArray(data) && Object.keys(data).length === 0) {
errors.push(this.errorReporter.createError('', ValidationErrorCode.EMPTY_INPUT, data, {
expected: 'non-empty RDFormat object'
}));
return { valid: false, errors, warnings };
}
// Handle empty arrays
if (Array.isArray(data) && data.length === 0) {
errors.push(this.errorReporter.createError('', ValidationErrorCode.EMPTY_ARRAY, data, {
expected: 'non-empty array of diagnostics'
}));
return { valid: false, errors, warnings };
}
// Validate against the schema
this.validateValue(data, this.schema, '', errors, warnings);
// Add additional RDFormat-specific validation for better error messages
this.addRDFormatSpecificValidation(data, '', errors, warnings);
return {
valid: errors.length === 0,
errors,
warnings
};
}
/**
* Performs additional edge case validation specific to RDFormat
*/
performEdgeCaseValidation(data, path, errors, warnings) {
// Handle partial diagnostic objects
if (this.isPartialDiagnostic(data)) {
this.validatePartialDiagnostic(data, path, errors, warnings);
}
// Handle arrays of partial diagnostics
if (Array.isArray(data)) {
for (let i = 0; i < data.length; i++) {
if (this.isPartialDiagnostic(data[i])) {
this.validatePartialDiagnostic(data[i], this.joinPath(path, i.toString()), errors, warnings);
}
}
}
// Handle diagnostic result objects
if (this.isDiagnosticResult(data)) {
this.validateDiagnosticResult(data, path, errors, warnings);
}
}
/**
* Checks if an object appears to be a partial diagnostic
*/
isPartialDiagnostic(obj) {
return typeof obj === 'object' &&
obj !== null &&
!Array.isArray(obj) &&
!obj.hasOwnProperty('diagnostics') && // Not a diagnostic result
(obj.hasOwnProperty('message') ||
obj.hasOwnProperty('location') ||
obj.hasOwnProperty('severity') ||
obj.hasOwnProperty('source'));
}
/**
* Checks if an object appears to be a diagnostic result
*/
isDiagnosticResult(obj) {
return typeof obj === 'object' &&
obj !== null &&
!Array.isArray(obj) &&
obj.hasOwnProperty('diagnostics');
}
/**
* Validates partial diagnostic objects and provides helpful error messages
*/
validatePartialDiagnostic(diagnostic, path, errors, warnings) {
// Check for missing required fields with specific error messages
if (!diagnostic.hasOwnProperty('message')) {
errors.push(this.errorReporter.createError(this.joinPath(path, 'message'), ValidationErrorCode.MISSING_DIAGNOSTIC_MESSAGE, undefined, {
expected: 'string containing the diagnostic message'
}));
}
if (!diagnostic.hasOwnProperty('location')) {
errors.push(this.errorReporter.createError(this.joinPath(path, 'location'), ValidationErrorCode.MISSING_DIAGNOSTIC_LOCATION, undefined, {
expected: 'object with path and optional range'
}));
}
// Validate location structure if present
if (diagnostic.location && typeof diagnostic.location === 'object') {
this.validateLocationStructure(diagnostic.location, this.joinPath(path, 'location'), errors, warnings);
}
// Validate severity if present
if (diagnostic.severity && !['UNKNOWN_SEVERITY', 'ERROR', 'WARNING', 'INFO'].includes(diagnostic.severity)) {
errors.push(this.errorReporter.createError(this.joinPath(path, 'severity'), ValidationErrorCode.INVALID_SEVERITY, diagnostic.severity, {
expected: 'one of: UNKNOWN_SEVERITY, ERROR, WARNING, INFO'
}));
}
}
/**
* Validates diagnostic result structure
*/
validateDiagnosticResult(result, path, errors, warnings) {
if (!Array.isArray(result.diagnostics)) {
errors.push(this.errorReporter.createError(this.joinPath(path, 'diagnostics'), ValidationErrorCode.TYPE_MISMATCH, result.diagnostics, {
expected: 'array',
suggestion: 'The diagnostics field must be an array of diagnostic objects.'
}));
}
else if (result.diagnostics.length === 0) {
warnings.push(this.errorReporter.createWarning(this.joinPath(path, 'diagnostics'), ValidationErrorCode.EMPTY_ARRAY, 'Diagnostic result contains no diagnostics. This may be intentional but is unusual.'));
}
}
/**
* Validates location structure for common issues
*/
validateLocationStructure(location, path, errors, warnings) {
if (!location.path || typeof location.path !== 'string') {
errors.push(this.errorReporter.createError(this.joinPath(path, 'path'), ValidationErrorCode.INVALID_LOCATION, location.path, {
expected: 'non-empty string representing the file path'
}));
}
else if (location.path.trim() === '') {
errors.push(this.errorReporter.createError(this.joinPath(path, 'path'), ValidationErrorCode.EMPTY_STRING, location.path, {
expected: 'non-empty file path'
}));
}
// Validate range if present
if (location.range) {
this.validateRangeStructure(location.range, this.joinPath(path, 'range'), errors, warnings);
}
}
/**
* Validates range structure for common issues
*/
validateRangeStructure(range, path, errors, warnings) {
if (!range.start) {
errors.push(this.errorReporter.createError(this.joinPath(path, 'start'), ValidationErrorCode.INVALID_RANGE, range.start, {
expected: 'position object with line number'
}));
}
else {
this.validatePositionStructure(range.start, this.joinPath(path, 'start'), errors, warnings);
}
if (range.end) {
this.validatePositionStructure(range.end, this.joinPath(path, 'end'), errors, warnings);
}
}
/**
* Validates position structure for common issues
*/
validatePositionStructure(position, path, errors, warnings) {
if (typeof position.line !== 'number' || position.line < 1) {
errors.push(this.errorReporter.createError(this.joinPath(path, 'line'), ValidationErrorCode.INVALID_POSITION, position.line, {
expected: 'positive integer (1-based line number)'
}));
}
if (position.column !== undefined && (typeof position.column !== 'number' || position.column < 1)) {
errors.push(this.errorReporter.createError(this.joinPath(path, 'column'), ValidationErrorCode.INVALID_POSITION, position.column, {
expected: 'positive integer (1-based column number) or undefined'
}));
}
}
/**
* Validates a specific field at a given path
*/
validateField(path, value, schema) {
const errors = [];
const warnings = [];
this.validateValue(value, schema, path, errors, warnings);
return {
valid: errors.length === 0,
errors,
warnings
};
}
/**
* Internal method to validate a value against a schema
*/
validateValue(value, schema, path, errors, warnings) {
// Handle oneOf schema (multiple possible formats)
if (schema.oneOf) {
let validationPassed = false;
const allErrors = [];
let bestErrors = [];
let bestWarnings = [];
let bestScore = -1;
// Sort schemas by likelihood based on the input type
const sortedSchemas = this.sortSchemasByLikelihood(schema.oneOf, value);
for (let i = 0; i < sortedSchemas.length; i++) {
const subSchema = sortedSchemas[i];
const subErrors = [];
const subWarnings = [];
this.validateValue(value, subSchema, path, subErrors, subWarnings);
if (subErrors.length === 0) {
validationPassed = true;
warnings.push(...subWarnings);
break;
}
else {
allErrors.push(...subErrors);
// Score based on how well the schema matches (fewer errors = better match)
const score = this.calculateSchemaMatchScore(value, subSchema, subErrors);
if (bestScore === -1 || score > bestScore) {
bestScore = score;
bestErrors = [...subErrors];
bestWarnings = [...subWarnings];
}
}
}
if (!validationPassed) {
// Add the most specific errors instead of a generic oneOf error
errors.push(...bestErrors);
warnings.push(...bestWarnings);
}
return;
}
// Handle type validation
if (schema.type) {
if (!this.validateType(value, schema.type)) {
errors.push(this.errorReporter.createError(path, ValidationErrorCode.TYPE_MISMATCH, value, {
expected: schema.type,
suggestion: `Please provide a ${schema.type} value.`
}));
return;
}
}
// Handle enum validation
if (schema.enum && !schema.enum.includes(value)) {
errors.push(this.errorReporter.createError(path, ValidationErrorCode.ENUM_VALIDATION_FAILED, value, {
expected: `one of: ${schema.enum.join(', ')}`
}));
return;
}
// Handle string constraints
if (schema.type === 'string' && typeof value === 'string') {
// Check for empty strings first (more specific than minLength)
if (schema.minLength !== undefined && schema.minLength > 0 && value.trim() === '') {
errors.push(this.errorReporter.createError(path, ValidationErrorCode.EMPTY_STRING, value, {
expected: 'non-empty string'
}));
}
else if (schema.minLength !== undefined && value.length < schema.minLength) {
errors.push(this.errorReporter.createError(path, ValidationErrorCode.MIN_LENGTH_VIOLATION, value, {
constraint: schema.minLength,
expected: `string with minimum length ${schema.minLength}`
}));
}
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
errors.push(this.errorReporter.createError(path, ValidationErrorCode.MAX_LENGTH_VIOLATION, value, {
constraint: schema.maxLength,
expected: `string with maximum length ${schema.maxLength}`
}));
}
if (schema.pattern && !new RegExp(schema.pattern).test(value)) {
errors.push(this.errorReporter.createError(path, ValidationErrorCode.PATTERN_MISMATCH, value, {
expected: `string matching pattern: ${schema.pattern}`
}));
}
}
// Handle number constraints
if (schema.type === 'number' && typeof value === 'number') {
if (schema.minimum !== undefined && value < schema.minimum) {
errors.push(this.errorReporter.createError(path, ValidationErrorCode.MIN_VALUE_VIOLATION, value, {
constraint: schema.minimum,
expected: `number >= ${schema.minimum}`
}));
}
if (schema.maximum !== undefined && value > schema.maximum) {
errors.push(this.errorReporter.createError(path, ValidationErrorCode.MAX_VALUE_VIOLATION, value, {
constraint: schema.maximum,
expected: `number <= ${schema.maximum}`
}));
}
}
// Handle object validation
if (schema.type === 'object' && typeof value === 'object' && value !== null) {
this.validateObject(value, schema, path, errors, warnings);
}
// Handle array validation
if (schema.type === 'array' && Array.isArray(value)) {
this.validateArray(value, schema, path, errors, warnings);
}
}
/**
* Validates an object against an object schema
*/
validateObject(obj, schema, path, errors, warnings) {
// Check required properties
if (schema.required) {
for (const requiredProp of schema.required) {
if (!(requiredProp in obj)) {
errors.push(this.errorReporter.createError(this.joinPath(path, requiredProp), ValidationErrorCode.REQUIRED_PROPERTY_MISSING, undefined, {
expected: `object with required property '${requiredProp}'`
}));
}
}
}
// Validate properties
if (schema.properties) {
for (const [propName, propSchema] of Object.entries(schema.properties)) {
if (propName in obj) {
this.validateValue(obj[propName], propSchema, this.joinPath(path, propName), errors, warnings);
}
}
}
// Handle extra properties
if (schema.properties && (schema.additionalProperties === false || !this.options.allowExtraFields)) {
const allowedProps = new Set(Object.keys(schema.properties));
for (const propName of Object.keys(obj)) {
if (!allowedProps.has(propName)) {
if (this.options.strictMode) {
errors.push(this.errorReporter.createError(this.joinPath(path, propName), ValidationErrorCode.UNKNOWN_PROPERTY, obj[propName], {
expected: 'property not present'
}));
}
else {
warnings.push(this.errorReporter.createWarning(this.joinPath(path, propName), ValidationErrorCode.UNKNOWN_PROPERTY));
}
}
}
}
}
/**
* Validates an array against an array schema
*/
validateArray(arr, schema, path, errors, warnings) {
if (schema.items) {
for (let i = 0; i < arr.length; i++) {
this.validateValue(arr[i], schema.items, this.joinPath(path, i.toString()), errors, warnings);
}
}
}
/**
* Validates the type of a value
*/
validateType(value, expectedType) {
switch (expectedType) {
case 'string':
return typeof value === 'string';
case 'number':
return typeof value === 'number' && !isNaN(value);
case 'boolean':
return typeof value === 'boolean';
case 'object':
return typeof value === 'object' && value !== null && !Array.isArray(value);
case 'array':
return Array.isArray(value);
case 'null':
return value === null;
default:
return false;
}
}
/**
* Joins path segments for error reporting
*/
joinPath(basePath, segment) {
if (!basePath)
return segment;
if (/^\d+$/.test(segment)) {
return `${basePath}[${segment}]`;
}
return `${basePath}.${segment}`;
}
/**
* Adds RDFormat-specific validation for better error messages
*/
addRDFormatSpecificValidation(data, path, errors, warnings) {
// Only add specific validation if we don't already have errors for the same issues
const existingErrorPaths = new Set(errors.map(e => e.path));
// Handle single diagnostic objects
if (this.isPartialDiagnostic(data)) {
// Replace generic REQUIRED_PROPERTY_MISSING with specific diagnostic errors
if (!data.hasOwnProperty('message')) {
// Remove generic error and add specific one
const genericIndex = errors.findIndex(e => e.path === this.joinPath(path, 'message') && e.code === ValidationErrorCode.REQUIRED_PROPERTY_MISSING);
if (genericIndex >= 0) {
errors.splice(genericIndex, 1);
}
errors.push(this.errorReporter.createError(this.joinPath(path, 'message'), ValidationErrorCode.MISSING_DIAGNOSTIC_MESSAGE, undefined, {
expected: 'string containing the diagnostic message'
}));
}
if (!data.hasOwnProperty('location')) {
// Remove generic error and add specific one
const genericIndex = errors.findIndex(e => e.path === this.joinPath(path, 'location') && e.code === ValidationErrorCode.REQUIRED_PROPERTY_MISSING);
if (genericIndex >= 0) {
errors.splice(genericIndex, 1);
}
errors.push(this.errorReporter.createError(this.joinPath(path, 'location'), ValidationErrorCode.MISSING_DIAGNOSTIC_LOCATION, undefined, {
expected: 'object with path and optional range'
}));
}
// Add specific validation for diagnostic fields
if (data.location && typeof data.location === 'object') {
this.addLocationSpecificValidation(data.location, this.joinPath(path, 'location'), errors, warnings);
}
// Add specific severity validation
if (data.severity && !['UNKNOWN_SEVERITY', 'ERROR', 'WARNING', 'INFO'].includes(data.severity)) {
// Replace generic enum error with specific severity error
const enumErrorIndex = errors.findIndex(e => e.path === this.joinPath(path, 'severity') && e.code === ValidationErrorCode.ENUM_VALIDATION_FAILED);
if (enumErrorIndex >= 0) {
errors.splice(enumErrorIndex, 1);
}
errors.push(this.errorReporter.createError(this.joinPath(path, 'severity'), ValidationErrorCode.INVALID_SEVERITY, data.severity, {
expected: 'one of: UNKNOWN_SEVERITY, ERROR, WARNING, INFO'
}));
}
}
// Handle arrays of diagnostics
if (Array.isArray(data)) {
for (let i = 0; i < data.length; i++) {
if (this.isPartialDiagnostic(data[i])) {
this.addRDFormatSpecificValidation(data[i], this.joinPath(path, i.toString()), errors, warnings);
}
}
}
// Handle diagnostic result objects
if (this.isDiagnosticResult(data)) {
if (Array.isArray(data.diagnostics)) {
for (let i = 0; i < data.diagnostics.length; i++) {
this.addRDFormatSpecificValidation(data.diagnostics[i], this.joinPath(path, `diagnostics[${i}]`), errors, warnings);
}
// Add warning for empty diagnostics array
if (data.diagnostics.length === 0) {
warnings.push(this.errorReporter.createWarning(this.joinPath(path, 'diagnostics'), ValidationErrorCode.EMPTY_ARRAY, 'Diagnostic result contains no diagnostics. This may be intentional but is unusual.'));
}
}
}
}
/**
* Adds location-specific validation for better error messages
*/
addLocationSpecificValidation(location, path, errors, warnings) {
// Add specific validation for ranges and positions
if (location.range && typeof location.range === 'object') {
if (!location.range.start) {
// Replace generic error with specific range error
const genericIndex = errors.findIndex(e => e.path === this.joinPath(path, 'range.start') && e.code === ValidationErrorCode.REQUIRED_PROPERTY_MISSING);
if (genericIndex >= 0) {
errors.splice(genericIndex, 1);
}
errors.push(this.errorReporter.createError(this.joinPath(path, 'range.start'), ValidationErrorCode.INVALID_RANGE, location.range.start, {
expected: 'position object with line number'
}));
}
else {
this.addPositionSpecificValidation(location.range.start, this.joinPath(path, 'range.start'), errors, warnings);
}
if (location.range.end) {
this.addPositionSpecificValidation(location.range.end, this.joinPath(path, 'range.end'), errors, warnings);
}
}
}
/**
* Adds position-specific validation for better error messages
*/
addPositionSpecificValidation(position, path, errors, warnings) {
if (typeof position.line !== 'number' || position.line < 1) {
// Replace generic error with specific position error
const minValueIndex = errors.findIndex(e => e.path === this.joinPath(path, 'line') && e.code === ValidationErrorCode.MIN_VALUE_VIOLATION);
if (minValueIndex >= 0) {
errors.splice(minValueIndex, 1);
}
errors.push(this.errorReporter.createError(this.joinPath(path, 'line'), ValidationErrorCode.INVALID_POSITION, position.line, {
expected: 'positive integer (1-based line number)'
}));
}
if (position.column !== undefined && (typeof position.column !== 'number' || position.column < 1)) {
// Replace generic error with specific position error
const minValueIndex = errors.findIndex(e => e.path === this.joinPath(path, 'column') && e.code === ValidationErrorCode.MIN_VALUE_VIOLATION);
if (minValueIndex >= 0) {
errors.splice(minValueIndex, 1);
}
errors.push(this.errorReporter.createError(this.joinPath(path, 'column'), ValidationErrorCode.INVALID_POSITION, position.column, {
expected: 'positive integer (1-based column number) or undefined'
}));
}
}
/**
* Updates validation options
*/
setOptions(options) {
this.options = { ...this.options, ...options };
}
/**
* Gets the current schema
*/
getSchema() {
return this.schema;
}
/**
* Sorts schemas by likelihood of matching the input value
*/
sortSchemasByLikelihood(schemas, value) {
return schemas.sort((a, b) => {
const scoreA = this.getSchemaLikelihoodScore(a, value);
const scoreB = this.getSchemaLikelihoodScore(b, value);
return scoreB - scoreA; // Higher score first
});
}
/**
* Calculates a likelihood score for how well a schema might match a value
*/
getSchemaLikelihoodScore(schema, value) {
let score = 0;
// Type matching
if (schema.type) {
if (this.validateType(value, schema.type)) {
score += 10;
}
else {
return 0; // If type doesn't match, this schema is unlikely
}
}
// Object structure matching
if (schema.type === 'object' && typeof value === 'object' && value !== null && !Array.isArray(value)) {
if (schema.required) {
const requiredProps = schema.required;
const valueProps = Object.keys(value);
const matchingRequired = requiredProps.filter(prop => valueProps.includes(prop));
// Strong bonus for matching all required properties
if (matchingRequired.length === requiredProps.length) {
score += 50;
}
else {
score += matchingRequired.length * 5;
// Heavy penalty for missing required properties
score -= (requiredProps.length - matchingRequired.length) * 20;
}
}
if (schema.properties) {
const schemaProps = Object.keys(schema.properties);
const valueProps = Object.keys(value);
const matchingProps = schemaProps.filter(prop => valueProps.includes(prop));
score += matchingProps.length * 2;
// Bonus for having properties that match the schema structure
const propertyMatchRatio = matchingProps.length / Math.max(valueProps.length, 1);
score += propertyMatchRatio * 10;
}
// Special handling for diagnostic-like objects
if (value.message && value.location && schema.required?.includes('message') && schema.required?.includes('location')) {
score += 30; // Strong bonus for diagnostic structure
}
// Penalty for diagnostic result structure when we have diagnostic fields
if (schema.required?.includes('diagnostics') && (value.message || value.location)) {
score -= 25; // This looks more like a single diagnostic
}
}
// Array matching
if (schema.type === 'array' && Array.isArray(value)) {
score += 15;
if (value.length > 0 && schema.items) {
// Check if first item matches the schema
const firstItemScore = this.getSchemaLikelihoodScore(schema.items, value[0]);
score += firstItemScore * 0.3;
}
}
return Math.max(0, score);
}
/**
* Calculates how well a schema matches based on validation errors
*/
calculateSchemaMatchScore(value, schema, errors) {
let score = 100; // Start with perfect score
// Penalty for each error
score -= errors.length * 5;
// Heavy penalty for type mismatches (these are fundamental)
const typeMismatchErrors = errors.filter(e => e.code === ValidationErrorCode.TYPE_MISMATCH);
score -= typeMismatchErrors.length * 50;
// Heavy penalty for missing required properties that suggest wrong schema
const missingPropErrors = errors.filter(e => e.code === ValidationErrorCode.REQUIRED_PROPERTY_MISSING);
score -= missingPropErrors.length * 30;
// Less penalty for validation errors within the correct structure
const structuralErrors = errors.filter(e => e.code === ValidationErrorCode.MIN_LENGTH_VIOLATION ||
e.code === ValidationErrorCode.MAX_LENGTH_VIOLATION ||
e.code === ValidationErrorCode.PATTERN_MISMATCH ||
e.code === ValidationErrorCode.EMPTY_STRING ||
e.code === ValidationErrorCode.MIN_VALUE_VIOLATION ||
e.code === ValidationErrorCode.MAX_VALUE_VIOLATION ||
e.code === ValidationErrorCode.ENUM_VALIDATION_FAILED ||
e.code === ValidationErrorCode.INVALID_POSITION ||
e.code === ValidationErrorCode.INVALID_RANGE ||
e.code === ValidationErrorCode.INVALID_LOCATION ||
e.code === ValidationErrorCode.INVALID_SEVERITY);
score += structuralErrors.length * 2; // Reduce penalty for these
// Bonus for schemas that match the input structure better
if (schema.type === 'object' && typeof value === 'object' && value !== null && !Array.isArray(value)) {
if (schema.required?.includes('message') && value.message) {
score += 10;
}
if (schema.required?.includes('location') && value.location) {
score += 10;
}
if (schema.required?.includes('diagnostics') && !value.diagnostics) {
score -= 20; // This is probably not a diagnostic result
}
}
return Math.max(0, score);
}
}
exports.Validator = Validator;
// Export convenience functions
function validate(data, options) {
const validator = new Validator(options);
return validator.validate(data);
}
function validateField(path, value, schema, options) {
const validator = new Validator(options);
return validator.validateField(path, value, schema);
}
//# sourceMappingURL=index.js.map