fast-validators 0.1.0__py3-none-win_amd64.whl
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.
- fast_validators/__init__.py +24 -0
- fast_validators/_validator_js_ts.dll +0 -0
- fast_validators/_validator_tree.dll +0 -0
- fast_validators/_validator_tree.py +73 -0
- fast_validators/base_validator.py +33 -0
- fast_validators/go_validator.py +25 -0
- fast_validators/js_ts_validator.py +50 -0
- fast_validators/json_validator.py +32 -0
- fast_validators/php_validator.py +25 -0
- fast_validators/python_validator.py +190 -0
- fast_validators/validators_catalog.py +78 -0
- fast_validators/yaml_validator.py +85 -0
- fast_validators-0.1.0.dist-info/METADATA +5 -0
- fast_validators-0.1.0.dist-info/RECORD +15 -0
- fast_validators-0.1.0.dist-info/WHEEL +6 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validators package for content validation in file operations.
|
|
3
|
+
|
|
4
|
+
This package provides a validation system for different file formats
|
|
5
|
+
to ensure content integrity after text replacement operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .base_validator import BaseValidator
|
|
9
|
+
from .json_validator import JsonValidator
|
|
10
|
+
from .yaml_validator import YamlValidator
|
|
11
|
+
from .php_validator import PhpValidator
|
|
12
|
+
from .go_validator import GoValidator
|
|
13
|
+
from .js_ts_validator import JS_TS_Validator
|
|
14
|
+
from .validators_catalog import ValidatorsCatalog
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
'BaseValidator',
|
|
18
|
+
'JsonValidator',
|
|
19
|
+
'YamlValidator',
|
|
20
|
+
'PhpValidator',
|
|
21
|
+
'GoValidator',
|
|
22
|
+
'JS_TS_Validator',
|
|
23
|
+
'ValidatorsCatalog'
|
|
24
|
+
]
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import ctypes
|
|
3
|
+
import platform
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Language(IntEnum):
|
|
9
|
+
CPP = 0
|
|
10
|
+
# _DEPRECATED_JAVASCRIPT = 1
|
|
11
|
+
# _DEPRECATED_TYPESCRIPT = 2
|
|
12
|
+
PHP = 3
|
|
13
|
+
GO = 4
|
|
14
|
+
# _DEPRECATED_TSX = 5
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def validate_syntax(source_code: str, language: Language, filename: str = "<code>") -> tuple[bool, str]:
|
|
18
|
+
# filename is used for display in error messages
|
|
19
|
+
|
|
20
|
+
lang_id = int(language)
|
|
21
|
+
debug_dump = False
|
|
22
|
+
c_result = _lib.validate_code(
|
|
23
|
+
source_code.encode("utf-8"),
|
|
24
|
+
filename.encode("utf-8"),
|
|
25
|
+
lang_id,
|
|
26
|
+
debug_dump,
|
|
27
|
+
)
|
|
28
|
+
try:
|
|
29
|
+
if not c_result.has_error:
|
|
30
|
+
return True, ""
|
|
31
|
+
return False, c_result.formatted_report.decode("utf-8")
|
|
32
|
+
finally:
|
|
33
|
+
_lib.free_validation_result(c_result)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# --- Private ctypes Implementation ---
|
|
37
|
+
|
|
38
|
+
class _Validation_Result(ctypes.Structure):
|
|
39
|
+
_fields_ = [
|
|
40
|
+
("has_error", ctypes.c_bool ),
|
|
41
|
+
("lineno", ctypes.c_uint32),
|
|
42
|
+
("column", ctypes.c_uint32),
|
|
43
|
+
("end_lineno", ctypes.c_uint32),
|
|
44
|
+
("end_column", ctypes.c_uint32),
|
|
45
|
+
("message", ctypes.c_char_p),
|
|
46
|
+
("formatted_report", ctypes.c_char_p),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
CURRENT_OS = platform.system().lower()
|
|
51
|
+
|
|
52
|
+
def _load_library(name: str):
|
|
53
|
+
if CURRENT_OS == "darwin":
|
|
54
|
+
LIB_EXT = ".dylib"
|
|
55
|
+
elif CURRENT_OS == "windows":
|
|
56
|
+
LIB_EXT = ".dll"
|
|
57
|
+
else:
|
|
58
|
+
LIB_EXT = ".so"
|
|
59
|
+
|
|
60
|
+
lib_path = Path(__file__).parent / f"{name}{LIB_EXT}"
|
|
61
|
+
if not lib_path.exists():
|
|
62
|
+
raise ImportError(f"Cannot find compiled library at {lib_path}. "
|
|
63
|
+
"Please build it first.")
|
|
64
|
+
|
|
65
|
+
return ctypes.CDLL(os.fsdecode(lib_path))
|
|
66
|
+
|
|
67
|
+
_lib = _load_library("_validator_tree")
|
|
68
|
+
|
|
69
|
+
_lib.validate_code.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_uint8, ctypes.c_bool]
|
|
70
|
+
_lib.validate_code.restype = _Validation_Result
|
|
71
|
+
|
|
72
|
+
_lib.free_validation_result.argtypes = [_Validation_Result]
|
|
73
|
+
_lib.free_validation_result.restype = None
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
class BaseValidator(ABC):
|
|
4
|
+
"""
|
|
5
|
+
Abstract base class for content validators.
|
|
6
|
+
Each validator checks specific file format validity.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def validate(self, new_text: str, filename: str) -> tuple[bool, str]:
|
|
11
|
+
"""
|
|
12
|
+
Validate the content for correctness.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
new_text: The content to validate
|
|
16
|
+
filename: The target filename (for context)
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
tuple: (is_valid: bool, error_message: str)
|
|
20
|
+
- is_valid: True if content is valid, False otherwise
|
|
21
|
+
- error_message: Empty string if valid, error description if invalid
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def supported_extensions(self) -> list[str]:
|
|
28
|
+
"""Return list of file extensions this validator supports"""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
def get_validation_name(self) -> str:
|
|
32
|
+
"""Return human-readable name of this validator"""
|
|
33
|
+
return self.__class__.__name__
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .base_validator import BaseValidator
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from ._validator_tree import validate_syntax, Language
|
|
5
|
+
LIBRARY_MISSING_ERROR = ""
|
|
6
|
+
except (ImportError, OSError) as e:
|
|
7
|
+
LIBRARY_MISSING_ERROR = (
|
|
8
|
+
"The GO validator component could not be loaded.\n"
|
|
9
|
+
"Please ensure the library has been built by running 'python nob.py build'.\n"
|
|
10
|
+
f"Details: {e}"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
class GoValidator(BaseValidator):
|
|
14
|
+
@property
|
|
15
|
+
def supported_extensions(self) -> list[str]:
|
|
16
|
+
return [".go"]
|
|
17
|
+
|
|
18
|
+
def validate(self, new_text: str, filename: str) -> tuple[bool, str]:
|
|
19
|
+
if LIBRARY_MISSING_ERROR:
|
|
20
|
+
return False, LIBRARY_MISSING_ERROR
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
return validate_syntax(new_text, Language.GO, filename)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
return False, f"An unexpected error occurred while validating {filename}: {str(e)}"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from .base_validator import BaseValidator
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from ._validator_tree import _load_library
|
|
7
|
+
|
|
8
|
+
_lib = _load_library("_validator_js_ts")
|
|
9
|
+
|
|
10
|
+
_lib.validate.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
|
|
11
|
+
_lib.validate.restype = ctypes.c_void_p
|
|
12
|
+
|
|
13
|
+
_lib.free_result.argtypes = [ctypes.c_void_p]
|
|
14
|
+
_lib.free_result.restype = None
|
|
15
|
+
|
|
16
|
+
LIBRARY_MISSING_ERROR = ""
|
|
17
|
+
except (ImportError, OSError) as e:
|
|
18
|
+
LIBRARY_MISSING_ERROR = (
|
|
19
|
+
"The JavaScript/TypeScript validator could not be loaded. "
|
|
20
|
+
"Please ensure the library has been built.\n"
|
|
21
|
+
f"Details: {e}"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class JS_TS_Validator(BaseValidator):
|
|
26
|
+
@property
|
|
27
|
+
def supported_extensions(self) -> list[str]:
|
|
28
|
+
return ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts']
|
|
29
|
+
|
|
30
|
+
def validate(self, new_text: str, filename: str) -> tuple[bool, str]:
|
|
31
|
+
if LIBRARY_MISSING_ERROR:
|
|
32
|
+
return False, LIBRARY_MISSING_ERROR
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
error_address = _lib.validate(
|
|
36
|
+
new_text.encode("utf-8"),
|
|
37
|
+
filename.encode("utf-8")
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
if not error_address:
|
|
42
|
+
return True, ""
|
|
43
|
+
|
|
44
|
+
error_string_ptr = ctypes.cast(error_address, ctypes.c_char_p)
|
|
45
|
+
error_message = error_string_ptr.value.decode("utf-8")
|
|
46
|
+
return False, error_message
|
|
47
|
+
finally:
|
|
48
|
+
_lib.free_result(error_address)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
return False, f"An unexpected error occurred while validating {filename}: {str(e)}"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from .base_validator import BaseValidator
|
|
3
|
+
|
|
4
|
+
class JsonValidator(BaseValidator):
|
|
5
|
+
"""Validator for JSON file content"""
|
|
6
|
+
|
|
7
|
+
def validate(self, new_text: str, filename: str) -> tuple[bool, str]:
|
|
8
|
+
"""
|
|
9
|
+
Validate JSON syntax and structure.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
tuple: (is_valid, error_message)
|
|
13
|
+
"""
|
|
14
|
+
try:
|
|
15
|
+
json.loads(new_text)
|
|
16
|
+
return True, ""
|
|
17
|
+
except json.JSONDecodeError as e:
|
|
18
|
+
error_msg = self._format_json_error(e, filename)
|
|
19
|
+
return False, error_msg
|
|
20
|
+
except Exception as e:
|
|
21
|
+
return False, f"Unexpected error validating JSON in {filename}: {str(e)}"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def supported_extensions(self) -> list[str]:
|
|
25
|
+
return ['.json', '.jsonl', '.geojson']
|
|
26
|
+
|
|
27
|
+
def _format_json_error(self, error: json.JSONDecodeError, filename: str) -> str:
|
|
28
|
+
"""Format JSON parsing error for user display"""
|
|
29
|
+
return (f"JSON validation failed for {filename}:\n"
|
|
30
|
+
f"Error: {error.msg}\n"
|
|
31
|
+
f"Line: {error.lineno}, Column: {error.colno}\n"
|
|
32
|
+
f"Position: {error.pos}")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .base_validator import BaseValidator
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from ._validator_tree import validate_syntax, Language
|
|
5
|
+
LIBRARY_MISSING_ERROR = ""
|
|
6
|
+
except (ImportError, OSError) as e:
|
|
7
|
+
LIBRARY_MISSING_ERROR = (
|
|
8
|
+
"The PHP validator component could not be loaded.\n"
|
|
9
|
+
"Please ensure the library has been built by running 'python nob.py build'.\n"
|
|
10
|
+
f"Details: {e}"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
class PhpValidator(BaseValidator):
|
|
14
|
+
@property
|
|
15
|
+
def supported_extensions(self) -> list[str]:
|
|
16
|
+
return [".php", ".phtml", ".phps", ".php3", ".php4", ".php5", ".php7", ".php8", ".pht"]
|
|
17
|
+
|
|
18
|
+
def validate(self, new_text: str, filename: str) -> tuple[bool, str]:
|
|
19
|
+
if LIBRARY_MISSING_ERROR:
|
|
20
|
+
return False, LIBRARY_MISSING_ERROR
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
return validate_syntax(new_text, Language.PHP, filename)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
return False, f"An unexpected error occurred while validating {filename}: {str(e)}"
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
from typing import List, Tuple, Set
|
|
5
|
+
from .base_validator import BaseValidator
|
|
6
|
+
|
|
7
|
+
class PythonValidator(BaseValidator):
|
|
8
|
+
"""Validator for Python code content"""
|
|
9
|
+
|
|
10
|
+
# Potentially dangerous patterns to check for
|
|
11
|
+
DANGEROUS_PATTERNS = [
|
|
12
|
+
(r'\beval\s*\(', 'Use of eval() can be dangerous'),
|
|
13
|
+
(r'\bexec\s*\(', 'Use of exec() can be dangerous'),
|
|
14
|
+
(r'__import__\s*\(', 'Dynamic imports with __import__ should be used carefully'),
|
|
15
|
+
(r'\bcompile\s*\(', 'Use of compile() should be reviewed'),
|
|
16
|
+
(r'subprocess\.call\s*\(.*shell\s*=\s*True', 'subprocess with shell=True can be dangerous'),
|
|
17
|
+
(r'os\.system\s*\(', 'os.system() can be dangerous, consider subprocess instead'),
|
|
18
|
+
(r'input\s*\(.*\beval\b', 'eval() in input() is dangerous'),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
def validate(self, new_text: str, filename: str) -> Tuple[bool, str]:
|
|
22
|
+
"""
|
|
23
|
+
Validate Python code syntax and basic quality checks.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
tuple: (is_valid, error_message)
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
# Handle empty or whitespace-only content
|
|
30
|
+
if not new_text or new_text.strip() == "":
|
|
31
|
+
return True, ""
|
|
32
|
+
|
|
33
|
+
# Check if file contains only comments and whitespace
|
|
34
|
+
if self._is_comments_only(new_text):
|
|
35
|
+
return True, ""
|
|
36
|
+
|
|
37
|
+
# 1. Syntax validation using AST
|
|
38
|
+
syntax_valid, syntax_error = self._validate_syntax(new_text, filename)
|
|
39
|
+
if not syntax_valid:
|
|
40
|
+
return False, syntax_error
|
|
41
|
+
|
|
42
|
+
# 2. Import validation
|
|
43
|
+
import_valid, import_error = self._validate_imports(new_text, filename)
|
|
44
|
+
if not import_valid:
|
|
45
|
+
return False, import_error
|
|
46
|
+
|
|
47
|
+
# 3. Basic code quality checks
|
|
48
|
+
quality_valid, quality_error = self._validate_code_quality(new_text, filename)
|
|
49
|
+
if not quality_valid:
|
|
50
|
+
return False, quality_error
|
|
51
|
+
|
|
52
|
+
# 4. Security checks (warnings, not failures)
|
|
53
|
+
security_warnings = self._check_security_patterns(new_text, filename)
|
|
54
|
+
if security_warnings:
|
|
55
|
+
# For now, just return warnings as part of error message
|
|
56
|
+
# In production, you might want to handle warnings differently
|
|
57
|
+
return False, f"Security warnings in {filename}:\n" + "\n".join(security_warnings)
|
|
58
|
+
|
|
59
|
+
return True, ""
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
return False, f"Unexpected error validating Python code in {filename}: {str(e)}"
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def supported_extensions(self) -> List[str]:
|
|
66
|
+
return ['.py', '.pyw']
|
|
67
|
+
|
|
68
|
+
def _validate_syntax(self, code: str, filename: str) -> Tuple[bool, str]:
|
|
69
|
+
"""Validate Python syntax using AST parsing"""
|
|
70
|
+
try:
|
|
71
|
+
ast.parse(code, filename=filename)
|
|
72
|
+
return True, ""
|
|
73
|
+
except SyntaxError as e:
|
|
74
|
+
# Check if this is a tabs/spaces mixing error
|
|
75
|
+
if "inconsistent use of tabs and spaces" in str(e):
|
|
76
|
+
return False, f"Mixed tabs and spaces for indentation in {filename}"
|
|
77
|
+
error_msg = self._format_syntax_error(e, filename)
|
|
78
|
+
return False, error_msg
|
|
79
|
+
except Exception as e:
|
|
80
|
+
return False, f"Syntax validation failed for {filename}: {str(e)}"
|
|
81
|
+
|
|
82
|
+
def _validate_imports(self, code: str, filename: str) -> Tuple[bool, str]:
|
|
83
|
+
"""Validate import statements"""
|
|
84
|
+
try:
|
|
85
|
+
tree = ast.parse(code, filename=filename)
|
|
86
|
+
|
|
87
|
+
# Check for problematic import patterns
|
|
88
|
+
for node in ast.walk(tree):
|
|
89
|
+
if isinstance(node, ast.Import):
|
|
90
|
+
for alias in node.names:
|
|
91
|
+
if alias.name.startswith('.'):
|
|
92
|
+
return False, f"Invalid import in {filename} line {node.lineno}: relative import '{alias.name}' not allowed in import statement"
|
|
93
|
+
|
|
94
|
+
elif isinstance(node, ast.ImportFrom):
|
|
95
|
+
# Check for excessive relative imports (4 or more dots)
|
|
96
|
+
if node.level and node.level >= 4:
|
|
97
|
+
return False, f"Excessive relative import in {filename} line {node.lineno}: too many parent directory references"
|
|
98
|
+
|
|
99
|
+
# Check for star imports (warning level)
|
|
100
|
+
for alias in node.names:
|
|
101
|
+
if alias.name == '*':
|
|
102
|
+
# This could be a warning instead of error
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
return True, ""
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
return False, f"Import validation failed for {filename}: {str(e)}"
|
|
109
|
+
|
|
110
|
+
def _validate_code_quality(self, code: str, filename: str) -> Tuple[bool, str]:
|
|
111
|
+
"""Basic code quality checks"""
|
|
112
|
+
lines = code.split('\n')
|
|
113
|
+
|
|
114
|
+
# Check for mixed tabs and spaces
|
|
115
|
+
has_tabs = any('\t' in line for line in lines)
|
|
116
|
+
has_spaces_indent = any(line.startswith(' ') for line in lines if line.strip())
|
|
117
|
+
|
|
118
|
+
if has_tabs and has_spaces_indent:
|
|
119
|
+
return False, f"Mixed tabs and spaces for indentation in {filename}"
|
|
120
|
+
|
|
121
|
+
# Check for extremely long lines (configurable threshold)
|
|
122
|
+
max_line_length = 1000
|
|
123
|
+
for i, line in enumerate(lines, 1):
|
|
124
|
+
if len(line) > max_line_length:
|
|
125
|
+
return False, f"Line too long in {filename} line {i}: {len(line)} characters (max {max_line_length})"
|
|
126
|
+
|
|
127
|
+
# Check for basic structure issues
|
|
128
|
+
try:
|
|
129
|
+
tree = ast.parse(code, filename=filename)
|
|
130
|
+
|
|
131
|
+
# Check for functions/classes with no content (only pass/docstring)
|
|
132
|
+
for node in ast.walk(tree):
|
|
133
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
134
|
+
if len(node.body) == 1:
|
|
135
|
+
first_stmt = node.body[0]
|
|
136
|
+
if isinstance(first_stmt, ast.Pass):
|
|
137
|
+
# This is just a warning, not an error
|
|
138
|
+
pass
|
|
139
|
+
elif isinstance(first_stmt, ast.Expr) and isinstance(first_stmt.value, ast.Constant):
|
|
140
|
+
# Just a docstring, might want to warn
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
return True, ""
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
return False, f"Code quality validation failed for {filename}: {str(e)}"
|
|
147
|
+
|
|
148
|
+
def _check_security_patterns(self, code: str, filename: str) -> List[str]:
|
|
149
|
+
"""Check for potentially dangerous code patterns"""
|
|
150
|
+
warnings = []
|
|
151
|
+
|
|
152
|
+
for pattern, message in self.DANGEROUS_PATTERNS:
|
|
153
|
+
matches = re.finditer(pattern, code, re.IGNORECASE | re.MULTILINE)
|
|
154
|
+
for match in matches:
|
|
155
|
+
# Find line number
|
|
156
|
+
line_num = code[:match.start()].count('\n') + 1
|
|
157
|
+
warnings.append(f"Line {line_num}: {message}")
|
|
158
|
+
|
|
159
|
+
return warnings
|
|
160
|
+
|
|
161
|
+
def _is_comments_only(self, code: str) -> bool:
|
|
162
|
+
"""Check if code contains only comments and whitespace"""
|
|
163
|
+
lines = code.split('\n')
|
|
164
|
+
for line in lines:
|
|
165
|
+
stripped = line.strip()
|
|
166
|
+
if stripped and not stripped.startswith('#'):
|
|
167
|
+
return False
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
def _format_syntax_error(self, error: SyntaxError, filename: str) -> str:
|
|
171
|
+
"""Format syntax error for user display"""
|
|
172
|
+
error_details = [f"Python syntax error in {filename}:"]
|
|
173
|
+
|
|
174
|
+
if error.msg:
|
|
175
|
+
error_details.append(f"Error: {error.msg}")
|
|
176
|
+
|
|
177
|
+
if error.lineno:
|
|
178
|
+
error_details.append(f"Line: {error.lineno}")
|
|
179
|
+
|
|
180
|
+
if error.offset:
|
|
181
|
+
error_details.append(f"Column: {error.offset}")
|
|
182
|
+
|
|
183
|
+
if error.text:
|
|
184
|
+
error_details.append(f"Code: {error.text.strip()}")
|
|
185
|
+
if error.offset:
|
|
186
|
+
# Add pointer to error location
|
|
187
|
+
pointer = ' ' * (error.offset - 1) + '^'
|
|
188
|
+
error_details.append(f" {pointer}")
|
|
189
|
+
|
|
190
|
+
return "\n".join(error_details)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from .base_validator import BaseValidator
|
|
3
|
+
from .json_validator import JsonValidator
|
|
4
|
+
from .yaml_validator import YamlValidator
|
|
5
|
+
from .python_validator import PythonValidator
|
|
6
|
+
from .php_validator import PhpValidator
|
|
7
|
+
from .go_validator import GoValidator
|
|
8
|
+
from .js_ts_validator import JS_TS_Validator
|
|
9
|
+
|
|
10
|
+
class ValidatorsCatalog:
|
|
11
|
+
"""
|
|
12
|
+
Central registry for all file content validators.
|
|
13
|
+
Uses simple includes instead of complex registration methods.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
# Simple includes - just instantiate the validators we want
|
|
18
|
+
self._validators = [
|
|
19
|
+
JsonValidator(),
|
|
20
|
+
YamlValidator(),
|
|
21
|
+
PythonValidator(),
|
|
22
|
+
PhpValidator(),
|
|
23
|
+
GoValidator(),
|
|
24
|
+
JS_TS_Validator(),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# Build extension to validator mapping for fast lookup
|
|
28
|
+
self._extension_map = {}
|
|
29
|
+
for validator in self._validators:
|
|
30
|
+
for ext in validator.supported_extensions:
|
|
31
|
+
self._extension_map[ext.lower()] = validator
|
|
32
|
+
|
|
33
|
+
def validate_content(self, new_text: str, filename: str) -> tuple[bool, str]:
|
|
34
|
+
"""
|
|
35
|
+
Validate content for a specific file.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
new_text: The new content to validate
|
|
39
|
+
filename: The target filename (used to determine validator)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
tuple: (is_valid: bool, error_message: str)
|
|
43
|
+
"""
|
|
44
|
+
validator = self.get_validator_for_file(filename)
|
|
45
|
+
if validator is None:
|
|
46
|
+
# No validator found for this file type - consider it valid
|
|
47
|
+
return True, ""
|
|
48
|
+
|
|
49
|
+
return validator.validate(new_text, filename)
|
|
50
|
+
|
|
51
|
+
def get_validator_for_file(self, filename: str) -> BaseValidator:
|
|
52
|
+
"""Get appropriate validator based on file extension"""
|
|
53
|
+
if not filename:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
# Extract file extension
|
|
57
|
+
_, ext = os.path.splitext(filename)
|
|
58
|
+
ext = ext.lower()
|
|
59
|
+
|
|
60
|
+
return self._extension_map.get(ext)
|
|
61
|
+
|
|
62
|
+
def get_supported_extensions(self) -> list[str]:
|
|
63
|
+
"""Get list of all supported file extensions"""
|
|
64
|
+
return list(self._extension_map.keys())
|
|
65
|
+
|
|
66
|
+
def get_validator(self, filename: str) -> BaseValidator:
|
|
67
|
+
"""Get appropriate validator based on file extension (alias for get_validator_for_file)"""
|
|
68
|
+
return self.get_validator_for_file(filename)
|
|
69
|
+
|
|
70
|
+
def get_validators_info(self) -> list[dict]:
|
|
71
|
+
"""Get information about all registered validators"""
|
|
72
|
+
info = []
|
|
73
|
+
for validator in self._validators:
|
|
74
|
+
info.append({
|
|
75
|
+
'name': validator.get_validation_name(),
|
|
76
|
+
'extensions': validator.supported_extensions
|
|
77
|
+
})
|
|
78
|
+
return info
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
from .base_validator import BaseValidator
|
|
3
|
+
|
|
4
|
+
class YamlValidator(BaseValidator):
|
|
5
|
+
"""Validator for YAML file content"""
|
|
6
|
+
|
|
7
|
+
def validate(self, new_text: str, filename: str) -> tuple[bool, str]:
|
|
8
|
+
"""
|
|
9
|
+
Validate YAML syntax and structure.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
tuple: (is_valid, error_message)
|
|
13
|
+
"""
|
|
14
|
+
try:
|
|
15
|
+
# Handle empty or whitespace-only content
|
|
16
|
+
if not new_text or new_text.strip() == "":
|
|
17
|
+
return True, ""
|
|
18
|
+
|
|
19
|
+
# Check for duplicate keys using a custom loader
|
|
20
|
+
self._check_duplicate_keys(new_text)
|
|
21
|
+
|
|
22
|
+
# Load YAML with safe loader - handle multi-document YAML
|
|
23
|
+
docs = list(yaml.safe_load_all(new_text))
|
|
24
|
+
return True, ""
|
|
25
|
+
except yaml.YAMLError as e:
|
|
26
|
+
error_msg = self._format_yaml_error(e, filename)
|
|
27
|
+
return False, error_msg
|
|
28
|
+
except Exception as e:
|
|
29
|
+
return False, f"Unexpected error validating YAML in {filename}: {str(e)}"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def supported_extensions(self) -> list[str]:
|
|
33
|
+
return ['.yaml', '.yml']
|
|
34
|
+
|
|
35
|
+
def _check_duplicate_keys(self, yaml_content: str):
|
|
36
|
+
"""Check for duplicate keys in YAML content"""
|
|
37
|
+
class DuplicateKeyLoader(yaml.SafeLoader):
|
|
38
|
+
def construct_mapping(self, node, deep=False):
|
|
39
|
+
mapping = {}
|
|
40
|
+
for key_node, value_node in node.value:
|
|
41
|
+
key = self.construct_object(key_node, deep=deep)
|
|
42
|
+
if key in mapping:
|
|
43
|
+
raise yaml.constructor.ConstructorError(
|
|
44
|
+
None, None,
|
|
45
|
+
f"found duplicate key: {key}",
|
|
46
|
+
key_node.start_mark
|
|
47
|
+
)
|
|
48
|
+
value = self.construct_object(value_node, deep=deep)
|
|
49
|
+
mapping[key] = value
|
|
50
|
+
return mapping
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# Load all documents in case of multi-document YAML
|
|
54
|
+
docs = list(yaml.load_all(yaml_content, Loader=DuplicateKeyLoader))
|
|
55
|
+
except yaml.constructor.ConstructorError as e:
|
|
56
|
+
if "duplicate key" in str(e):
|
|
57
|
+
raise yaml.YAMLError(f"duplicate key found: {str(e)}")
|
|
58
|
+
|
|
59
|
+
def _format_yaml_error(self, error: yaml.YAMLError, filename: str) -> str:
|
|
60
|
+
"""Format YAML parsing error for user display"""
|
|
61
|
+
error_details = []
|
|
62
|
+
error_details.append(f"yaml validation failed for {filename}:")
|
|
63
|
+
|
|
64
|
+
if hasattr(error, 'problem'):
|
|
65
|
+
problem = error.problem
|
|
66
|
+
# Replace \\t with 'tab' for better readability - handle all possible patterns
|
|
67
|
+
problem = problem.replace("'\\\\t'", "'tab'")
|
|
68
|
+
problem = problem.replace('\\\\t', 'tab')
|
|
69
|
+
problem = problem.replace("'\\t'", "'tab'")
|
|
70
|
+
problem = problem.replace('\\t', 'tab')
|
|
71
|
+
error_details.append(f"problem: {problem}")
|
|
72
|
+
|
|
73
|
+
if hasattr(error, 'problem_mark'):
|
|
74
|
+
mark = error.problem_mark
|
|
75
|
+
error_details.append(f"line: {mark.line + 1}, column: {mark.column + 1}")
|
|
76
|
+
|
|
77
|
+
if hasattr(error, 'context'):
|
|
78
|
+
error_details.append(f"context: {error.context}")
|
|
79
|
+
|
|
80
|
+
# Handle duplicate key errors
|
|
81
|
+
error_str = str(error)
|
|
82
|
+
if "duplicate key" in error_str.lower():
|
|
83
|
+
error_details = [f"yaml validation failed for {filename}:", f"duplicate key detected"]
|
|
84
|
+
|
|
85
|
+
return "\n".join(error_details)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
fast_validators-0.1.0.dist-info/METADATA,sha256=PxLy04HTrokj1OeseiRteVSc_hO6nR4miMPg5NG_4Hk,140
|
|
2
|
+
fast_validators-0.1.0.dist-info/RECORD,,
|
|
3
|
+
fast_validators-0.1.0.dist-info/WHEEL,sha256=_bOreq1gx9SJE5Dzh5pOxe8hEzRe-UVEK4LPtxfwHI0,160
|
|
4
|
+
fast_validators/__init__.py,sha256=NYsyPj5Z127_nvU5oyl6lIIgxzHEfzj2NdMG-ISxDmY,691
|
|
5
|
+
fast_validators/_validator_js_ts.dll,sha256=Ti_YrlqSLAHpQ6usgODti4miePdGrGi388jPL12sT_w,18341888
|
|
6
|
+
fast_validators/_validator_tree.dll,sha256=c44X05uSjdZy5PlfkxQfUHMgOAznGM2jn1oShtenxyY,6605312
|
|
7
|
+
fast_validators/_validator_tree.py,sha256=DTRCMUdnaigXNH8bI9fgo3ir599T0jfA4UEUILP51JA,2087
|
|
8
|
+
fast_validators/base_validator.py,sha256=7F_E5Xio837F2AudbDlW2wZ84amVHop-ytCt6LVZAiA,1078
|
|
9
|
+
fast_validators/go_validator.py,sha256=geAS04Dl9s-21CII7Q-xpE0jpC1ojp0ZwQZru_hMh-w,893
|
|
10
|
+
fast_validators/js_ts_validator.py,sha256=QKpQ_hCwcAQv4Rhdp7A9iuW9vy6aCfS5109-Jih6nEc,1651
|
|
11
|
+
fast_validators/json_validator.py,sha256=PfLMaenjRh25jnK1S7qF5qtQjp4bO1L_HN-Hi_QGsZY,1162
|
|
12
|
+
fast_validators/php_validator.py,sha256=O7bVc4vVYgSMouMiGACZZCw9FtiYPwaSdbFQM41BJl4,969
|
|
13
|
+
fast_validators/python_validator.py,sha256=cbiotwfeLXugHRk4wyaSxUdN_h43UHPyCvZiXzKyrhQ,8439
|
|
14
|
+
fast_validators/validators_catalog.py,sha256=lYWTWGUgur3-hLGQoQ2yg7FCTW2Vls4HDm5Nr6GLTxo,2772
|
|
15
|
+
fast_validators/yaml_validator.py,sha256=KyRCIYHyQtTeYmvSJoTtchDs_L-DveQ5zy5T_RJ8ang,3519
|