@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
472 lines • 17.6 kB
JavaScript
/**
* Fixer module for automatically correcting common RDFormat validation errors
*/
import { ValidationErrorCode } from '../validator/index.js';
/**
* Fixer class that can automatically correct common validation errors
*/
export class Fixer {
constructor(options = {}) {
this.options = {
strictMode: false,
fixLevel: 'basic',
...options
};
}
/**
* Attempts to fix validation errors in the provided data
*/
fix(data, validationResult) {
const appliedFixes = [];
const remainingErrors = [];
const fixedData = this.deepClone(data);
// Process each validation error and attempt to fix it
for (const error of validationResult.errors) {
const fixResult = this.applyFix(fixedData, error);
if (fixResult) {
appliedFixes.push(fixResult);
}
else {
remainingErrors.push(error);
}
}
return {
fixed: appliedFixes.length > 0,
data: fixedData,
appliedFixes,
remainingErrors
};
}
/**
* Checks if a specific validation error can be automatically fixed
*/
canFix(error) {
switch (error.code) {
// Type coercion fixes
case ValidationErrorCode.TYPE_MISMATCH:
return this.canFixTypeMismatch(error);
// Missing field fixes
case ValidationErrorCode.REQUIRED_PROPERTY_MISSING:
return this.canFixMissingProperty(error);
// String fixes
case ValidationErrorCode.EMPTY_STRING:
return this.options.fixLevel === 'aggressive';
// Number fixes
case ValidationErrorCode.MIN_VALUE_VIOLATION:
case ValidationErrorCode.MAX_VALUE_VIOLATION:
return this.options.fixLevel === 'aggressive';
// RDFormat specific fixes
case ValidationErrorCode.MISSING_DIAGNOSTIC_MESSAGE:
case ValidationErrorCode.MISSING_DIAGNOSTIC_LOCATION:
return true;
case ValidationErrorCode.INVALID_SEVERITY:
return true;
case ValidationErrorCode.INVALID_POSITION:
return this.options.fixLevel === 'aggressive';
default:
return false;
}
}
/**
* Applies a fix for a specific validation error
*/
applyFix(data, error) {
if (!this.canFix(error)) {
return null;
}
const pathParts = this.parsePath(error.path);
const before = this.getValueAtPath(data, pathParts);
let fixed = false;
let after;
switch (error.code) {
case ValidationErrorCode.TYPE_MISMATCH:
after = this.fixTypeMismatch(data, pathParts, error);
fixed = after !== undefined;
break;
case ValidationErrorCode.REQUIRED_PROPERTY_MISSING:
case ValidationErrorCode.MISSING_DIAGNOSTIC_MESSAGE:
case ValidationErrorCode.MISSING_DIAGNOSTIC_LOCATION:
after = this.fixMissingProperty(data, pathParts, error);
fixed = after !== undefined;
break;
case ValidationErrorCode.EMPTY_STRING:
after = this.fixEmptyString(data, pathParts, error);
fixed = after !== undefined;
break;
case ValidationErrorCode.MIN_VALUE_VIOLATION:
case ValidationErrorCode.MAX_VALUE_VIOLATION:
after = this.fixNumberViolation(data, pathParts, error);
fixed = after !== undefined;
break;
case ValidationErrorCode.INVALID_SEVERITY:
after = this.fixInvalidSeverity(data, pathParts, error);
fixed = after !== undefined;
break;
case ValidationErrorCode.INVALID_POSITION:
after = this.fixInvalidPosition(data, pathParts, error);
fixed = after !== undefined;
break;
default:
return null;
}
if (fixed) {
return {
path: error.path,
message: this.getFixMessage(error.code, before, after),
before,
after
};
}
return null;
}
/**
* Fixes type mismatches through coercion
*/
fixTypeMismatch(data, pathParts, error) {
const currentValue = this.getValueAtPath(data, pathParts);
const expectedType = this.extractExpectedType(error.expected || '');
let fixedValue;
switch (expectedType) {
case 'string':
if (typeof currentValue === 'number' || typeof currentValue === 'boolean') {
fixedValue = String(currentValue);
}
else if (currentValue === null || currentValue === undefined) {
fixedValue = '';
}
break;
case 'number':
if (typeof currentValue === 'string' && !isNaN(Number(currentValue))) {
fixedValue = Number(currentValue);
}
else if (typeof currentValue === 'boolean') {
fixedValue = currentValue ? 1 : 0;
}
break;
case 'boolean':
if (typeof currentValue === 'string') {
fixedValue = currentValue.toLowerCase() === 'true';
}
else if (typeof currentValue === 'number') {
fixedValue = currentValue !== 0;
}
break;
case 'array':
if (!Array.isArray(currentValue) && currentValue !== null && currentValue !== undefined) {
fixedValue = [currentValue];
}
break;
case 'object':
if (typeof currentValue !== 'object' || Array.isArray(currentValue)) {
fixedValue = {};
}
break;
}
if (fixedValue !== undefined) {
this.setValueAtPath(data, pathParts, fixedValue);
return fixedValue;
}
return undefined;
}
/**
* Fixes missing required properties by adding default values
*/
fixMissingProperty(data, pathParts, error) {
const propertyName = pathParts[pathParts.length - 1];
let defaultValue;
// Determine default value based on property name and context
switch (propertyName) {
case 'message':
defaultValue = 'No message provided';
break;
case 'location':
defaultValue = { path: 'unknown' };
break;
case 'path':
defaultValue = 'unknown';
break;
case 'line':
defaultValue = 1;
break;
case 'column':
defaultValue = 1;
break;
case 'severity':
defaultValue = 'UNKNOWN_SEVERITY';
break;
case 'diagnostics':
defaultValue = [];
break;
case 'name':
// For source.name
defaultValue = 'unknown';
break;
default:
// Generic defaults based on expected type
if (error.expected?.includes('string')) {
defaultValue = '';
}
else if (error.expected?.includes('number')) {
defaultValue = 0;
}
else if (error.expected?.includes('array')) {
defaultValue = [];
}
else if (error.expected?.includes('object')) {
defaultValue = {};
}
else {
return undefined;
}
}
this.setValueAtPath(data, pathParts, defaultValue);
return defaultValue;
}
/**
* Fixes empty strings with meaningful defaults
*/
fixEmptyString(data, pathParts, _error) {
if (this.options.fixLevel !== 'aggressive') {
return undefined;
}
const propertyName = pathParts[pathParts.length - 1];
let defaultValue;
switch (propertyName) {
case 'message':
defaultValue = 'No message provided';
break;
case 'path':
defaultValue = 'unknown';
break;
case 'name':
defaultValue = 'unknown';
break;
default:
defaultValue = 'unknown';
}
this.setValueAtPath(data, pathParts, defaultValue);
return defaultValue;
}
/**
* Fixes number constraint violations
*/
fixNumberViolation(data, pathParts, error) {
if (this.options.fixLevel !== 'aggressive') {
return undefined;
}
const currentValue = this.getValueAtPath(data, pathParts);
let fixedValue;
if (error.code === ValidationErrorCode.MIN_VALUE_VIOLATION) {
// Extract minimum value from error message or use 1 as default
const minMatch = error.message.match(/at least (\d+)/);
const minValue = minMatch ? parseInt(minMatch[1]) : 1;
fixedValue = Math.max(currentValue, minValue);
}
else if (error.code === ValidationErrorCode.MAX_VALUE_VIOLATION) {
// Extract maximum value from error message or use reasonable default
const maxMatch = error.message.match(/at most (\d+)/);
const maxValue = maxMatch ? parseInt(maxMatch[1]) : 1000;
fixedValue = Math.min(currentValue, maxValue);
}
else {
return undefined;
}
this.setValueAtPath(data, pathParts, fixedValue);
return fixedValue;
}
/**
* Fixes invalid severity values
*/
fixInvalidSeverity(data, pathParts, _error) {
const currentValue = this.getValueAtPath(data, pathParts);
let fixedValue;
// Try to map common severity values to valid ones
if (typeof currentValue === 'string') {
const normalized = currentValue.toLowerCase();
switch (normalized) {
case 'error':
case 'err':
case 'fatal':
fixedValue = 'ERROR';
break;
case 'warning':
case 'warn':
case 'caution':
fixedValue = 'WARNING';
break;
case 'info':
case 'information':
case 'note':
fixedValue = 'INFO';
break;
default:
fixedValue = 'UNKNOWN_SEVERITY';
}
}
else {
fixedValue = 'UNKNOWN_SEVERITY';
}
this.setValueAtPath(data, pathParts, fixedValue);
return fixedValue;
}
/**
* Fixes invalid position values
*/
fixInvalidPosition(data, pathParts, _error) {
if (this.options.fixLevel !== 'aggressive') {
return undefined;
}
const currentValue = this.getValueAtPath(data, pathParts);
const propertyName = pathParts[pathParts.length - 1];
let fixedValue;
if (propertyName === 'line' || propertyName === 'column') {
if (typeof currentValue === 'number' && currentValue < 1) {
fixedValue = 1;
}
else if (typeof currentValue !== 'number') {
fixedValue = 1;
}
else {
return undefined;
}
this.setValueAtPath(data, pathParts, fixedValue);
return fixedValue;
}
return undefined;
}
/**
* Helper methods for path manipulation and value access
*/
parsePath(path) {
if (!path)
return [];
return path.split(/[.[\]]/).filter(part => part !== '');
}
getValueAtPath(obj, pathParts) {
let current = obj;
for (const part of pathParts) {
if (current === null || current === undefined) {
return undefined;
}
current = current[part];
}
return current;
}
setValueAtPath(obj, pathParts, value) {
if (pathParts.length === 0)
return;
let current = obj;
// Navigate to the parent of the target property
for (let i = 0; i < pathParts.length - 1; i++) {
const part = pathParts[i];
if (current[part] === undefined || current[part] === null) {
// Create intermediate objects/arrays as needed
const nextPart = pathParts[i + 1];
current[part] = /^\d+$/.test(nextPart) ? [] : {};
}
current = current[part];
}
// Set the final value
const finalPart = pathParts[pathParts.length - 1];
current[finalPart] = value;
}
deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => this.deepClone(item));
}
const cloned = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = this.deepClone(obj[key]);
}
}
return cloned;
}
extractExpectedType(expected) {
if (expected.includes('string'))
return 'string';
if (expected.includes('number'))
return 'number';
if (expected.includes('boolean'))
return 'boolean';
if (expected.includes('array'))
return 'array';
if (expected.includes('object'))
return 'object';
return 'unknown';
}
canFixTypeMismatch(error) {
const expectedType = this.extractExpectedType(error.expected || '');
const currentValue = error.value;
switch (expectedType) {
case 'string':
return typeof currentValue === 'number' ||
typeof currentValue === 'boolean' ||
currentValue === null ||
currentValue === undefined;
case 'number':
return (typeof currentValue === 'string' && !isNaN(Number(currentValue))) ||
typeof currentValue === 'boolean';
case 'boolean':
return typeof currentValue === 'string' ||
typeof currentValue === 'number';
case 'array':
return !Array.isArray(currentValue) &&
currentValue !== null &&
currentValue !== undefined;
case 'object':
return typeof currentValue !== 'object' ||
Array.isArray(currentValue);
default:
return false;
}
}
canFixMissingProperty(error) {
const pathParts = this.parsePath(error.path);
const propertyName = pathParts[pathParts.length - 1];
// We can fix most missing properties with reasonable defaults
const fixableProperties = [
'message', 'location', 'path', 'line', 'column',
'severity', 'diagnostics', 'name'
];
return fixableProperties.includes(propertyName) ||
(error.expected?.includes('string') ?? false) ||
(error.expected?.includes('number') ?? false) ||
(error.expected?.includes('array') ?? false) ||
(error.expected?.includes('object') ?? false);
}
getFixMessage(code, before, after) {
switch (code) {
case ValidationErrorCode.TYPE_MISMATCH:
return `Converted ${typeof before} value '${before}' to ${typeof after} '${after}'`;
case ValidationErrorCode.REQUIRED_PROPERTY_MISSING:
case ValidationErrorCode.MISSING_DIAGNOSTIC_MESSAGE:
case ValidationErrorCode.MISSING_DIAGNOSTIC_LOCATION:
return `Added missing property with default value '${after}'`;
case ValidationErrorCode.EMPTY_STRING:
return `Replaced empty string with default value '${after}'`;
case ValidationErrorCode.MIN_VALUE_VIOLATION:
return `Adjusted value from ${before} to minimum allowed value ${after}`;
case ValidationErrorCode.MAX_VALUE_VIOLATION:
return `Adjusted value from ${before} to maximum allowed value ${after}`;
case ValidationErrorCode.INVALID_SEVERITY:
return `Normalized severity from '${before}' to '${after}'`;
case ValidationErrorCode.INVALID_POSITION:
return `Fixed invalid position value from ${before} to ${after}`;
default:
return `Fixed value from '${before}' to '${after}'`;
}
}
}
// Export convenience functions
export function createFixer(options) {
return new Fixer(options);
}
export function fixData(data, validationResult, options) {
const fixer = new Fixer(options);
return fixer.fix(data, validationResult);
}
//# sourceMappingURL=index.js.map