invar-tools 1.15.6__py3-none-any.whl → 1.17.0__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.
@@ -159,11 +159,13 @@ def text_with_pattern(
159
159
  )
160
160
  noise_lines = st.lists(noise_line, min_size=0, max_size=10)
161
161
 
162
+ # Note: Hypothesis will automatically explore different orderings of the lines,
163
+ # so explicit shuffling is unnecessary. The concatenation order is sufficient.
162
164
  return st.builds(
163
165
  lambda p, n: "\n".join(p + n),
164
166
  pattern_lines,
165
167
  noise_lines,
166
- ).map(lambda x: "\n".join(sorted(x.split("\n"), key=lambda _: __import__("random").random())))
168
+ )
167
169
 
168
170
 
169
171
  @pre(lambda pattern: len(pattern) > 0)
invar/mcp/handlers.py CHANGED
@@ -24,6 +24,11 @@ def _validate_path(path: str) -> tuple[bool, str]:
24
24
 
25
25
  Returns (is_valid, error_message).
26
26
  Rejects paths that could be interpreted as shell commands or flags.
27
+
28
+ Note: This validation is for MCP (Model Context Protocol) handlers, which
29
+ are designed to provide AI agents with access to the project filesystem.
30
+ We validate format and reject shell injection patterns, but do not restrict
31
+ to working directory (unlike CLI tools) since MCP is a trusted local protocol.
27
32
  """
28
33
  if not path:
29
34
  return True, "" # Empty path defaults to "." in handlers
@@ -38,9 +43,13 @@ def _validate_path(path: str) -> tuple[bool, str]:
38
43
  if char in path:
39
44
  return False, f"Invalid path: contains forbidden character: {char!r}"
40
45
 
41
- # Try to resolve path - this catches malformed paths
46
+ # Resolve path to canonical form, following symlinks
47
+ # This ensures path is valid and catches directory traversal attempts
42
48
  try:
43
49
  Path(path).resolve()
50
+ # Note: We don't restrict to cwd here because MCP handlers are designed
51
+ # to access the full project. If path restriction is needed, implement
52
+ # at the MCP server level, not per-handler.
44
53
  except (OSError, ValueError) as e:
45
54
  return False, f"Invalid path: {e}"
46
55
 
@@ -0,0 +1,17 @@
1
+ # Embedded Node.js tools - runtime dependencies
2
+ # These are installed by scripts/embed_node_tools.py
3
+ # Run that script before building or testing
4
+
5
+ */node_modules/
6
+ */package.json
7
+
8
+ # eslint-plugin is unbundled (entire dist/ directory)
9
+ # All other tools are bundled (single cli.js file)
10
+ eslint-plugin/
11
+
12
+ # Common generated/temp files
13
+ *.log
14
+ .DS_Store
15
+ .npm/
16
+ npm-debug.log*
17
+ node_modules/.cache/
invar/node_tools/MANIFEST CHANGED
@@ -2,6 +2,7 @@
2
2
  # Auto-generated by scripts/embed_node_tools.py
3
3
  # Do not edit manually
4
4
 
5
+ eslint-plugin
5
6
  fc-runner
6
7
  quick-check
7
8
  ts-analyzer
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI for @invar/eslint-plugin
4
+ *
5
+ * Runs ESLint with @invar/* rules pre-configured.
6
+ * Outputs standard ESLint JSON format for integration with guard_ts.py.
7
+ *
8
+ * Usage:
9
+ * node cli.js [path] [--config=recommended|strict]
10
+ *
11
+ * Options:
12
+ * path Project directory to lint (default: current directory)
13
+ * --config Use 'recommended' or 'strict' preset (default: recommended)
14
+ * --help Show help message
15
+ */
16
+ import { ESLint } from 'eslint';
17
+ import { resolve, dirname } from 'path';
18
+ import { fileURLToPath } from 'url';
19
+ import { statSync, realpathSync } from 'fs';
20
+ import plugin from './index.js';
21
+ // Get directory containing this CLI script (for resolving node_modules)
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = dirname(__filename);
24
+ function parseArgs(args) {
25
+ const projectPath = args.find(arg => !arg.startsWith('--')) || '.';
26
+ const configArg = args.find(arg => arg.startsWith('--config='));
27
+ const config = configArg?.split('=')[1] === 'strict' ? 'strict' : 'recommended';
28
+ const help = args.includes('--help') || args.includes('-h');
29
+ return { projectPath, config, help };
30
+ }
31
+ function printHelp() {
32
+ console.log(`
33
+ @invar/eslint-plugin - ESLint with Invar-specific rules
34
+
35
+ Usage:
36
+ node cli.js [path] [options]
37
+
38
+ Arguments:
39
+ path Project directory to lint (default: current directory)
40
+
41
+ Options:
42
+ --config=MODE Use 'recommended' or 'strict' preset (default: recommended)
43
+ --help, -h Show this help message
44
+
45
+ Examples:
46
+ node cli.js # Lint current directory (recommended mode)
47
+ node cli.js ./src # Lint specific directory
48
+ node cli.js --config=strict # Use strict mode (all rules as errors)
49
+
50
+ Output:
51
+ JSON format compatible with ESLint's --format=json
52
+ Exit code 0 if no errors, 1 if errors found
53
+ `);
54
+ }
55
+ async function main() {
56
+ const args = parseArgs(process.argv.slice(2));
57
+ if (args.help) {
58
+ printHelp();
59
+ process.exit(0);
60
+ }
61
+ const projectPath = resolve(args.projectPath);
62
+ // Validate resolved path is within current working directory
63
+ // This prevents path traversal attacks via "../../../etc/passwd" patterns
64
+ // and symlink-based bypasses (e.g., "./symlink_inside/../../../etc/passwd")
65
+ const cwd = process.cwd();
66
+ try {
67
+ // Use realpath to resolve symlinks and prevent bypass attacks
68
+ const realProjectPath = realpathSync(projectPath);
69
+ const realCwd = realpathSync(cwd);
70
+ if (!realProjectPath.startsWith(realCwd)) {
71
+ console.error(`Error: Project path must be within current directory`);
72
+ console.error(` Requested: ${args.projectPath}`);
73
+ console.error(` Resolved: ${realProjectPath}`);
74
+ console.error(` Working dir: ${realCwd}`);
75
+ process.exit(1);
76
+ }
77
+ }
78
+ catch (error) {
79
+ // If realpath fails (path doesn't exist), fall back to string comparison
80
+ // This allows error messages to be more specific
81
+ if (!projectPath.startsWith(cwd)) {
82
+ console.error(`Error: Project path must be within current directory`);
83
+ console.error(` Requested: ${args.projectPath}`);
84
+ console.error(` Resolved: ${projectPath}`);
85
+ console.error(` Working dir: ${cwd}`);
86
+ process.exit(1);
87
+ }
88
+ }
89
+ try {
90
+ // Get the rules config for the selected mode
91
+ const selectedConfig = plugin.configs?.[args.config];
92
+ if (!selectedConfig || !selectedConfig.rules) {
93
+ console.error(`Config "${args.config}" not found or invalid`);
94
+ process.exit(1);
95
+ }
96
+ // Create ESLint instance with programmatic configuration
97
+ // Set cwd to CLI directory so ESLint can find parser in our node_modules
98
+ const eslint = new ESLint({
99
+ useEslintrc: false, // Don't load .eslintrc files
100
+ cwd: __dirname, // Set working directory to CLI location for module resolution
101
+ baseConfig: {
102
+ parser: '@typescript-eslint/parser',
103
+ parserOptions: {
104
+ ecmaVersion: 2022,
105
+ sourceType: 'module',
106
+ },
107
+ plugins: ['@invar'],
108
+ rules: selectedConfig.rules,
109
+ },
110
+ plugins: {
111
+ '@invar': plugin, // Register our plugin programmatically
112
+ },
113
+ }); // Type assertion for ESLint config complexity
114
+ // Lint the project - detect if path is a file or directory
115
+ // ESLint defaults to .js only, so we need glob patterns for .ts/.tsx
116
+ let filesToLint;
117
+ try {
118
+ const stats = statSync(projectPath);
119
+ // Note: Advisory check for optimization - TOCTOU race condition is acceptable
120
+ // because ESLint will handle file system changes gracefully during actual linting
121
+ if (stats.isFile()) {
122
+ // Single file - lint it directly
123
+ filesToLint = [projectPath];
124
+ }
125
+ else if (stats.isDirectory()) {
126
+ // Directory - use glob patterns for TypeScript files primarily
127
+ // Note: Focus on TypeScript files as this is a TypeScript Guard tool
128
+ filesToLint = [
129
+ `${projectPath}/**/*.ts`,
130
+ `${projectPath}/**/*.tsx`,
131
+ ];
132
+ }
133
+ else {
134
+ console.error(`Error: Path is neither a file nor a directory: ${projectPath}`);
135
+ process.exit(1);
136
+ }
137
+ }
138
+ catch (error) {
139
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
140
+ console.error(`Error: Cannot access path: ${errorMessage}`);
141
+ process.exit(1);
142
+ }
143
+ const results = await eslint.lintFiles(filesToLint);
144
+ // Output in standard ESLint JSON format (compatible with guard_ts.py)
145
+ const formatter = await eslint.loadFormatter('json');
146
+ const resultText = await Promise.resolve(formatter.format(results, {
147
+ cwd: projectPath,
148
+ rulesMeta: eslint.getRulesMetaForResults(results),
149
+ }));
150
+ console.log(resultText);
151
+ // Exit with error code if there are errors
152
+ const hasErrors = results.some(result => result.errorCount > 0);
153
+ process.exit(hasErrors ? 1 : 0);
154
+ }
155
+ catch (error) {
156
+ // Sanitize error message to avoid leaking file paths or system information
157
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
158
+ console.error(`ESLint failed: ${errorMessage}`);
159
+ process.exit(1);
160
+ }
161
+ }
162
+ main();
163
+ //# sourceMappingURL=cli.js.map