invar-tools 1.17.12__py3-none-any.whl → 1.17.24__py3-none-any.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.
invar/core/models.py CHANGED
@@ -160,7 +160,10 @@ def get_layer(file_info: FileInfo) -> CodeLayer:
160
160
  return CodeLayer.DEFAULT
161
161
 
162
162
 
163
- @pre(lambda layer, language="python": isinstance(layer, CodeLayer) and language in ("python", "typescript"))
163
+ @pre(
164
+ lambda layer, language="python": isinstance(layer, CodeLayer)
165
+ and language in ("python", "typescript")
166
+ )
164
167
  @post(lambda result: result.max_file_lines > 0 and result.max_function_lines > 0)
165
168
  def get_limits(layer: CodeLayer, language: str = "python") -> LayerLimits:
166
169
  """
@@ -424,8 +427,9 @@ class RuleConfig(BaseModel):
424
427
  """
425
428
 
426
429
  # MINOR-6: Added ge=1 constraints for numeric fields
427
- max_file_lines: int = Field(default=500, ge=1) # Phase 9 P1: Raised from 300
428
- max_function_lines: int = Field(default=50, ge=1)
430
+ # BUG-55: These override layer-based limits when set to non-default values
431
+ max_file_lines: int = Field(default=500, ge=1) # Override all layers if != 500
432
+ max_function_lines: int = Field(default=50, ge=1) # Override all layers if != 50
429
433
  entry_max_lines: int = Field(default=15, ge=1) # DX-23: Entry point max lines
430
434
  shell_max_branches: int = Field(default=3, ge=1) # DX-22: Shell function max branches
431
435
  shell_complexity_debt_limit: int = Field(default=5, ge=0) # DX-22: 0 = no limit
invar/core/rules.py CHANGED
@@ -71,7 +71,11 @@ def _build_size_suggestion(base: str, extraction_hint: str, func_hint: str) -> s
71
71
  def _get_func_hint(file_info: FileInfo) -> str:
72
72
  """Get top 5 largest functions as hint string."""
73
73
  funcs = sorted(
74
- [(s.name, s.end_line - s.line + 1) for s in file_info.symbols if s.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD)],
74
+ [
75
+ (s.name, s.end_line - s.line + 1)
76
+ for s in file_info.symbols
77
+ if s.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD)
78
+ ],
75
79
  key=lambda x: -x[1],
76
80
  )[:5]
77
81
  return f" Functions: {', '.join(f'{n}({sz}L)' for n, sz in funcs)}" if funcs else ""
@@ -104,6 +108,7 @@ def check_file_size(file_info: FileInfo, config: RuleConfig) -> list[Violation]:
104
108
  Check if file exceeds maximum line count or warning threshold.
105
109
 
106
110
  LX-10: Uses layer-based limits (Core/Shell/Tests/Default).
111
+ BUG-55: Config override - if max_file_lines is set to non-default, use it.
107
112
  P18: Shows function groups in size warnings to help agents decide what to extract.
108
113
  P25: Shows extractable groups with dependencies for warnings.
109
114
 
@@ -121,6 +126,10 @@ def check_file_size(file_info: FileInfo, config: RuleConfig) -> list[Violation]:
121
126
  >>> # Core layer: 500 lines max (strict)
122
127
  >>> len(check_file_size(FileInfo(path="core/calc.py", lines=550, is_core=True), RuleConfig()))
123
128
  1
129
+ >>> # BUG-55: Config override allows larger files (no error at 550 with max 600)
130
+ >>> vs = check_file_size(FileInfo(path="core/calc.py", lines=550, is_core=True), RuleConfig(max_file_lines=600, size_warning_threshold=0))
131
+ >>> any(v.rule == "file_size" for v in vs)
132
+ False
124
133
  """
125
134
  # Check for escape hatch
126
135
  if _has_file_escape(file_info, "file_size"):
@@ -133,23 +142,38 @@ def check_file_size(file_info: FileInfo, config: RuleConfig) -> list[Violation]:
133
142
  # LX-10: Get layer-based limits
134
143
  layer = get_layer(file_info)
135
144
  limits = get_limits(layer)
136
- max_lines = limits.max_file_lines
145
+ # BUG-55: Allow config override if user sets non-default value
146
+ max_lines = config.max_file_lines if config.max_file_lines != 500 else limits.max_file_lines
137
147
 
138
148
  if file_info.lines > max_lines:
139
- violations.append(Violation(
140
- rule="file_size", severity=Severity.ERROR, file=file_info.path, line=None,
141
- message=f"File has {file_info.lines} lines (max: {max_lines} for {layer.value})",
142
- suggestion=_build_size_suggestion("Split into smaller modules.", extraction_hint, func_hint),
143
- ))
149
+ violations.append(
150
+ Violation(
151
+ rule="file_size",
152
+ severity=Severity.ERROR,
153
+ file=file_info.path,
154
+ line=None,
155
+ message=f"File has {file_info.lines} lines (max: {max_lines} for {layer.value})",
156
+ suggestion=_build_size_suggestion(
157
+ "Split into smaller modules.", extraction_hint, func_hint
158
+ ),
159
+ )
160
+ )
144
161
  elif config.size_warning_threshold > 0:
145
162
  threshold = int(max_lines * config.size_warning_threshold)
146
163
  if file_info.lines >= threshold:
147
164
  pct = int(file_info.lines / max_lines * 100)
148
- violations.append(Violation(
149
- rule="file_size_warning", severity=Severity.WARNING, file=file_info.path, line=None,
150
- message=f"File has {file_info.lines} lines ({pct}% of {max_lines} limit)",
151
- suggestion=_build_size_suggestion("Consider splitting before reaching limit.", extraction_hint, func_hint),
152
- ))
165
+ violations.append(
166
+ Violation(
167
+ rule="file_size_warning",
168
+ severity=Severity.WARNING,
169
+ file=file_info.path,
170
+ line=None,
171
+ message=f"File has {file_info.lines} lines ({pct}% of {max_lines} limit)",
172
+ suggestion=_build_size_suggestion(
173
+ "Consider splitting before reaching limit.", extraction_hint, func_hint
174
+ ),
175
+ )
176
+ )
153
177
  return violations
154
178
 
155
179
 
@@ -159,6 +183,7 @@ def check_function_size(file_info: FileInfo, config: RuleConfig) -> list[Violati
159
183
  Check if any function exceeds maximum line count.
160
184
 
161
185
  LX-10: Uses layer-based limits (Core/Shell/Tests/Default).
186
+ BUG-55: Config override - if max_function_lines is set to non-default, use it.
162
187
  DX-22: Always uses code_lines (excluding docstring) and excludes doctest lines.
163
188
 
164
189
  Examples:
@@ -177,13 +202,19 @@ def check_function_size(file_info: FileInfo, config: RuleConfig) -> list[Violati
177
202
  >>> info3 = FileInfo(path="core/calc.py", lines=100, symbols=[sym3], is_core=True)
178
203
  >>> len(check_function_size(info3, RuleConfig()))
179
204
  1
205
+ >>> # BUG-55: Config override allows larger functions
206
+ >>> len(check_function_size(info3, RuleConfig(max_function_lines=70)))
207
+ 0
180
208
  """
181
209
  violations: list[Violation] = []
182
210
 
183
211
  # LX-10: Get layer-based limits
184
212
  layer = get_layer(file_info)
185
213
  limits = get_limits(layer)
186
- max_func_lines = limits.max_function_lines
214
+ # BUG-55: Allow config override if user sets non-default value
215
+ max_func_lines = (
216
+ config.max_function_lines if config.max_function_lines != 50 else limits.max_function_lines
217
+ )
187
218
 
188
219
  for symbol in file_info.symbols:
189
220
  if symbol.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD):
@@ -392,7 +423,9 @@ def check_shell_result(file_info: FileInfo, config: RuleConfig) -> list[Violatio
392
423
  ):
393
424
  continue
394
425
  # DX-23: Skip entry points; DX-22: Skip if @invar:allow marker
395
- if is_entry_point(symbol, file_info.source) or has_allow_marker(symbol, file_info.source, "shell_result"):
426
+ if is_entry_point(symbol, file_info.source) or has_allow_marker(
427
+ symbol, file_info.source, "shell_result"
428
+ ):
396
429
  continue
397
430
  if "Result[" not in symbol.signature:
398
431
  violations.append(
@@ -435,7 +468,9 @@ def check_entry_point_thin(file_info: FileInfo, config: RuleConfig) -> list[Viol
435
468
  continue
436
469
 
437
470
  # Only check entry points; DX-22: Skip if @invar:allow marker
438
- if not is_entry_point(symbol, file_info.source) or has_allow_marker(symbol, file_info.source, "entry_point_too_thick"):
471
+ if not is_entry_point(symbol, file_info.source) or has_allow_marker(
472
+ symbol, file_info.source, "entry_point_too_thick"
473
+ ):
439
474
  continue
440
475
  lines = get_symbol_lines(symbol)
441
476
  if lines > max_lines:
invar/mcp/handlers.py CHANGED
@@ -427,11 +427,24 @@ async def _execute_command(
427
427
  timeout=timeout,
428
428
  )
429
429
 
430
+ stdout = result.stdout.strip()
431
+
432
+ # Try to parse as JSON
430
433
  try:
431
- parsed = json.loads(result.stdout)
434
+ parsed = json.loads(stdout)
432
435
  return ([TextContent(type="text", text=json.dumps(parsed, indent=2))], parsed)
433
436
  except json.JSONDecodeError:
434
- output = result.stdout
437
+ # Try to fix unescaped newlines in JSON strings
438
+ # Guard/map commands may output multiline JSON with literal newlines
439
+ fixed = _fix_json_newlines(stdout)
440
+ try:
441
+ parsed = json.loads(fixed)
442
+ return ([TextContent(type="text", text=json.dumps(parsed, indent=2))], parsed)
443
+ except json.JSONDecodeError:
444
+ pass
445
+
446
+ # Fall back to text output
447
+ output = stdout
435
448
  if result.stderr:
436
449
  output += f"\n\nStderr:\n{result.stderr}"
437
450
  return [TextContent(type="text", text=output)]
@@ -440,3 +453,46 @@ async def _execute_command(
440
453
  return [TextContent(type="text", text=f"Error: Command timed out ({timeout}s)")]
441
454
  except Exception as e:
442
455
  return [TextContent(type="text", text=f"Error: {e}")]
456
+
457
+
458
+ # @invar:allow shell_too_complex: Simple state machine, 6 branches is minimal
459
+ # @invar:allow shell_pure_logic: No I/O, but called from shell context
460
+ # @invar:allow shell_result: Pure transformation, returns str not Result
461
+ def _fix_json_newlines(text: str) -> str:
462
+ """Fix unescaped newlines in JSON strings.
463
+
464
+ When subprocess outputs multiline JSON, newlines inside string values
465
+ are not escaped, causing json.loads() to fail. This function escapes them.
466
+
467
+ DX-33: Escape hatch for complex pure logic helper.
468
+ """
469
+ result = []
470
+ i = 0
471
+ while i < len(text):
472
+ if text[i] == '"':
473
+ # Inside a string - collect until closing quote
474
+ result.append('"')
475
+ i += 1
476
+ while i < len(text):
477
+ c = text[i]
478
+ if c == "\\" and i + 1 < len(text):
479
+ # Escaped character - keep as is
480
+ result.append("\\")
481
+ result.append(text[i + 1])
482
+ i += 2
483
+ elif c == '"':
484
+ # End of string
485
+ result.append('"')
486
+ i += 1
487
+ break
488
+ elif c == "\n" or c == "\r":
489
+ # Unescaped newline - escape it
490
+ result.append("\\n")
491
+ i += 1
492
+ else:
493
+ result.append(c)
494
+ i += 1
495
+ else:
496
+ result.append(text[i])
497
+ i += 1
498
+ return "".join(result)
@@ -16,11 +16,61 @@
16
16
  import { ESLint } from 'eslint';
17
17
  import { resolve, dirname } from 'path';
18
18
  import { statSync, realpathSync } from 'fs';
19
+ import { spawnSync } from 'child_process';
19
20
  import { fileURLToPath } from 'url';
21
+ import { createRequire } from 'module';
20
22
  import plugin from './index.js';
21
23
  // Get the directory where this CLI script is located (embedded in site-packages)
22
24
  const __filename = fileURLToPath(import.meta.url);
23
25
  const __dirname = dirname(__filename);
26
+
27
+ const require = createRequire(import.meta.url);
28
+
29
+ function resolveTsParser(projectPath) {
30
+ try {
31
+ const tseslintEntry = require.resolve('typescript-eslint', { paths: [projectPath] });
32
+ if (tseslintEntry) {
33
+ const tseslintRoot = dirname(dirname(tseslintEntry));
34
+ return require.resolve('@typescript-eslint/parser', { paths: [tseslintRoot] });
35
+ }
36
+ }
37
+ catch {
38
+ }
39
+
40
+ try {
41
+ return require.resolve('@typescript-eslint/parser', { paths: [projectPath] });
42
+ }
43
+ catch {
44
+ }
45
+
46
+ try {
47
+ return require.resolve('@typescript-eslint/parser', { paths: [__dirname] });
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ function _gitLsFiles(projectPath) {
54
+ const check = spawnSync('git', ['-C', projectPath, 'rev-parse', '--is-inside-work-tree'], {
55
+ encoding: 'utf8',
56
+ timeout: 2000,
57
+ });
58
+ if (check.status !== 0) {
59
+ return null;
60
+ }
61
+
62
+ const ls = spawnSync('git', ['-C', projectPath, 'ls-files', '-z', '--', '*.ts', '*.tsx'], {
63
+ encoding: 'utf8',
64
+ timeout: 15000,
65
+ });
66
+ if (ls.status !== 0 || !ls.stdout) {
67
+ return null;
68
+ }
69
+
70
+ const files = ls.stdout.split('\0').filter(Boolean);
71
+ return files.length > 0 ? files : null;
72
+ }
73
+
24
74
  function parseArgs(args) {
25
75
  const projectPath = args.find(arg => !arg.startsWith('--')) || '.';
26
76
  const configArg = args.find(arg => arg.startsWith('--config='));
@@ -93,44 +143,35 @@ async function main() {
93
143
  console.error(`Config "${args.config}" not found or invalid`);
94
144
  process.exit(1);
95
145
  }
96
- // Create ESLint instance with programmatic configuration
97
- // Use __dirname (where CLI is located) for module resolution
98
- // This allows ESLint to find embedded node_modules in site-packages
99
- const eslint = new ESLint({
100
- useEslintrc: false, // Don't load .eslintrc files
101
- cwd: __dirname, // Use CLI location for module resolution (embedded node_modules)
102
- resolvePluginsRelativeTo: __dirname, // Resolve plugins from embedded location
103
- baseConfig: {
104
- parser: '@typescript-eslint/parser', // Will resolve from __dirname/node_modules
105
- parserOptions: {
106
- ecmaVersion: 2022,
107
- sourceType: 'module',
108
- },
109
- plugins: ['@invar'],
110
- rules: selectedConfig.rules,
111
- },
112
- plugins: {
113
- '@invar': plugin, // Register plugin directly
114
- },
115
- }); // Type assertion for ESLint config complexity
116
- // Lint the project - detect if path is a file or directory
117
- // ESLint defaults to .js only, so we need glob patterns for .ts/.tsx
146
+ const tsParser = resolveTsParser(projectPath);
147
+ if (!tsParser) {
148
+ console.error("ESLint failed: Failed to load TypeScript parser.");
149
+ console.error("Install either 'typescript-eslint' or '@typescript-eslint/parser' in your project.");
150
+ process.exit(1);
151
+ }
152
+
118
153
  let filesToLint;
154
+ let lintCwd = projectPath;
155
+ let globInputPaths = true;
119
156
  try {
120
157
  const stats = statSync(projectPath);
121
- // Note: Advisory check for optimization - TOCTOU race condition is acceptable
122
- // because ESLint will handle file system changes gracefully during actual linting
123
158
  if (stats.isFile()) {
124
- // Single file - lint it directly
159
+ lintCwd = dirname(projectPath);
125
160
  filesToLint = [projectPath];
161
+ globInputPaths = false;
126
162
  }
127
163
  else if (stats.isDirectory()) {
128
- // Directory - use glob patterns for TypeScript files primarily
129
- // Note: Focus on TypeScript files as this is a TypeScript Guard tool
130
- filesToLint = [
131
- `${projectPath}/**/*.ts`,
132
- `${projectPath}/**/*.tsx`,
133
- ];
164
+ const gitFiles = _gitLsFiles(projectPath);
165
+ if (gitFiles) {
166
+ filesToLint = gitFiles;
167
+ globInputPaths = false;
168
+ }
169
+ else {
170
+ filesToLint = [
171
+ "**/*.ts",
172
+ "**/*.tsx",
173
+ ];
174
+ }
134
175
  }
135
176
  else {
136
177
  console.error(`Error: Path is neither a file nor a directory: ${projectPath}`);
@@ -142,6 +183,39 @@ async function main() {
142
183
  console.error(`Error: Cannot access path: ${errorMessage}`);
143
184
  process.exit(1);
144
185
  }
186
+
187
+ const eslint = new ESLint({
188
+ useEslintrc: false,
189
+ cwd: lintCwd,
190
+ resolvePluginsRelativeTo: __dirname,
191
+ errorOnUnmatchedPattern: false,
192
+ globInputPaths,
193
+ baseConfig: {
194
+ parser: tsParser,
195
+ parserOptions: {
196
+ ecmaVersion: 2022,
197
+ sourceType: 'module',
198
+ },
199
+ plugins: ['@invar'],
200
+ rules: selectedConfig.rules,
201
+ ignorePatterns: [
202
+ '**/node_modules/**',
203
+ '**/.next/**',
204
+ '**/dist/**',
205
+ '**/build/**',
206
+ '**/.cache/**',
207
+ '**/coverage/**',
208
+ '**/.turbo/**',
209
+ '**/.vercel/**',
210
+ '**/playwright-report/**',
211
+ '**/test-results/**',
212
+ ],
213
+ },
214
+ plugins: {
215
+ '@invar': plugin, // Register plugin directly
216
+ },
217
+ });
218
+
145
219
  const results = await eslint.lintFiles(filesToLint);
146
220
  // Output in standard ESLint JSON format (compatible with guard_ts.py)
147
221
  const formatter = await eslint.loadFormatter('json');
@@ -77,49 +77,58 @@ function matchesEnforcePattern(filePath, patterns) {
77
77
  function isZodType(typeAnnotation) {
78
78
  return ZOD_TYPE_PATTERNS.some(pattern => pattern.test(typeAnnotation));
79
79
  }
80
- function hasParseCall(body, paramName) {
80
+ function collectParseArgs(body, visitorKeys) {
81
+ const parsed = new Set();
81
82
  if (!body)
82
- return false;
83
- let found = false;
84
- const MAX_DEPTH = 50; // Prevent stack overflow on deeply nested types
85
- const visit = (node, depth = 0) => {
86
- if (found)
87
- return;
83
+ return parsed;
84
+
85
+ const MAX_DEPTH = 50;
86
+ const stack = [{ node: body, depth: 0 }];
87
+
88
+ while (stack.length > 0) {
89
+ const current = stack.pop();
90
+ if (!current)
91
+ continue;
92
+ const node = current.node;
93
+ const depth = current.depth;
94
+ if (!node || typeof node !== 'object')
95
+ continue;
88
96
  if (depth > MAX_DEPTH)
89
- return; // Depth limit to prevent stack overflow
97
+ continue;
98
+
90
99
  if (node.type === 'CallExpression') {
91
100
  const callee = node.callee;
92
- if (callee.type === 'MemberExpression') {
101
+ if (callee && callee.type === 'MemberExpression') {
93
102
  const property = callee.property;
94
- if (property.type === 'Identifier' &&
95
- (property.name === 'parse' || property.name === 'safeParse')) {
96
- // Check if argument is our param
97
- if (node.arguments.some(arg => arg.type === 'Identifier' && arg.name === paramName)) {
98
- found = true;
99
- return;
103
+ if (property && property.type === 'Identifier' && (property.name === 'parse' || property.name === 'safeParse')) {
104
+ for (const arg of node.arguments || []) {
105
+ if (arg && arg.type === 'Identifier') {
106
+ parsed.add(arg.name);
107
+ }
100
108
  }
101
109
  }
102
110
  }
103
111
  }
104
- // Recursively visit children with depth tracking
105
- for (const key of Object.keys(node)) {
112
+
113
+ const keys = (visitorKeys && node.type && visitorKeys[node.type]) || [];
114
+ for (const key of keys) {
106
115
  const value = node[key];
107
- if (value && typeof value === 'object') {
108
- if (Array.isArray(value)) {
109
- for (const item of value) {
110
- if (item && typeof item === 'object' && 'type' in item) {
111
- visit(item, depth + 1);
112
- }
116
+ if (!value)
117
+ continue;
118
+ if (Array.isArray(value)) {
119
+ for (const item of value) {
120
+ if (item && typeof item === 'object' && item.type) {
121
+ stack.push({ node: item, depth: depth + 1 });
113
122
  }
114
123
  }
115
- else if ('type' in value) {
116
- visit(value, depth + 1);
117
- }
124
+ }
125
+ else if (typeof value === 'object' && value.type) {
126
+ stack.push({ node: value, depth: depth + 1 });
118
127
  }
119
128
  }
120
- };
121
- visit(body);
122
- return found;
129
+ }
130
+
131
+ return parsed;
123
132
  }
124
133
  export const requireSchemaValidation = {
125
134
  meta: {
@@ -206,49 +215,54 @@ export const requireSchemaValidation = {
206
215
  }
207
216
  function checkFunction(node, params) {
208
217
  const functionName = getFunctionName(node);
209
- // Skip if shouldn't check based on mode
210
218
  if (!shouldCheck(functionName)) {
211
219
  return;
212
220
  }
221
+
213
222
  const body = 'body' in node ? node.body : null;
223
+ const zodParams = params.filter((p) => p.typeAnnotation && isZodType(p.typeAnnotation) && p.name && p.name !== '{...}' && p.name !== '[...]');
224
+ if (zodParams.length === 0) {
225
+ return;
226
+ }
227
+
228
+ const parsedArgs = collectParseArgs(body, sourceCode.visitorKeys);
214
229
  const isRiskFunction = isHighRiskFunction(functionName, filename);
215
- for (const param of params) {
216
- if (param.typeAnnotation && isZodType(param.typeAnnotation)) {
217
- if (!hasParseCall(body, param.name)) {
218
- // Extract schema name from type annotation (e.g., "z.infer<typeof UserSchema>" -> "UserSchema")
219
- const schemaMatch = param.typeAnnotation.match(/typeof\s+(\w+)/);
220
- const schemaName = schemaMatch ? schemaMatch[1] : 'Schema';
221
- const validatedVarName = `validated${param.name.charAt(0).toUpperCase()}${param.name.slice(1)}`;
222
- context.report({
223
- node: node,
224
- messageId: isRiskFunction ? 'missingValidationRisk' : 'missingValidation',
225
- data: {
226
- name: param.name,
227
- functionName: functionName,
228
- },
229
- suggest: [
230
- {
231
- messageId: 'addParseCall',
232
- data: { name: param.name },
233
- fix(fixer) {
234
- // Find the opening brace of the function body
235
- if (!body || body.type !== 'BlockStatement')
236
- return null;
237
- const blockBody = body;
238
- if (!blockBody.body || blockBody.body.length === 0)
239
- return null;
240
- const firstStatement = blockBody.body[0];
241
- // Detect indentation from the first statement
242
- const firstStatementStart = firstStatement.loc?.start.column ?? 2;
243
- const indent = ' '.repeat(firstStatementStart);
244
- const parseCode = `const ${validatedVarName} = ${schemaName}.parse(${param.name});\n${indent}`;
245
- return fixer.insertTextBefore(firstStatement, parseCode);
246
- },
247
- },
248
- ],
249
- });
250
- }
230
+
231
+ for (const param of zodParams) {
232
+ if (parsedArgs.has(param.name)) {
233
+ continue;
251
234
  }
235
+
236
+ const schemaMatch = param.typeAnnotation.match(/typeof\s+(\w+)/);
237
+ const schemaName = schemaMatch ? schemaMatch[1] : 'Schema';
238
+ const validatedVarName = `validated${param.name.charAt(0).toUpperCase()}${param.name.slice(1)}`;
239
+
240
+ context.report({
241
+ node: node,
242
+ messageId: isRiskFunction ? 'missingValidationRisk' : 'missingValidation',
243
+ data: {
244
+ name: param.name,
245
+ functionName: functionName,
246
+ },
247
+ suggest: [
248
+ {
249
+ messageId: 'addParseCall',
250
+ data: { name: param.name },
251
+ fix(fixer) {
252
+ if (!body || body.type !== 'BlockStatement')
253
+ return null;
254
+ const blockBody = body;
255
+ if (!blockBody.body || blockBody.body.length === 0)
256
+ return null;
257
+ const firstStatement = blockBody.body[0];
258
+ const firstStatementStart = firstStatement.loc?.start.column ?? 2;
259
+ const indent = ' '.repeat(firstStatementStart);
260
+ const parseCode = `const ${validatedVarName} = ${schemaName}.parse(${param.name});\n${indent}`;
261
+ return fixer.insertTextBefore(firstStatement, parseCode);
262
+ },
263
+ },
264
+ ],
265
+ });
252
266
  }
253
267
  }
254
268
  /**