tova 0.10.3 → 0.11.12

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.
@@ -0,0 +1,154 @@
1
+ // src/cli/check.js — Type-checking command
2
+ import { resolve, dirname, relative } from 'path';
3
+ import { readFileSync, existsSync, statSync } from 'fs';
4
+ import { Lexer } from '../lexer/lexer.js';
5
+ import { Parser } from '../parser/parser.js';
6
+ import { Analyzer } from '../analyzer/analyzer.js';
7
+ import { richError, DiagnosticFormatter, formatSummary } from '../diagnostics/formatter.js';
8
+ import { getExplanation, lookupCode } from '../diagnostics/error-codes.js';
9
+ import { generateSecurityScorecard } from '../diagnostics/security-scorecard.js';
10
+ import { findFiles } from './utils.js';
11
+
12
+ async function checkProject(args) {
13
+ const checkStrict = args.includes('--strict');
14
+ const checkStrictSecurity = args.includes('--strict-security');
15
+ const isVerbose = args.includes('--verbose');
16
+ const isQuiet = args.includes('--quiet');
17
+
18
+ // --explain <code>: show explanation for a specific error code inline with check output
19
+ const explainIdx = args.indexOf('--explain');
20
+ const explainCode = explainIdx >= 0 ? args[explainIdx + 1] : null;
21
+ if (explainCode) {
22
+ // If --explain is used standalone, just show the explanation
23
+ const info = lookupCode(explainCode);
24
+ if (!info) {
25
+ console.error(`Unknown error code: ${explainCode}`);
26
+ process.exit(1);
27
+ }
28
+ const explanation = getExplanation(explainCode);
29
+ console.log(`\n ${explainCode}: ${info.title} [${info.category}]\n`);
30
+ if (explanation) {
31
+ console.log(explanation);
32
+ } else {
33
+ console.log(` No detailed explanation available yet for ${explainCode}.\n`);
34
+ }
35
+ process.exit(0);
36
+ }
37
+
38
+ const explicitSrc = args.filter(a => !a.startsWith('--'))[0];
39
+ const srcPath = resolve(explicitSrc || '.');
40
+
41
+ // Support both single file and directory arguments
42
+ let tovaFiles;
43
+ if (existsSync(srcPath) && statSync(srcPath).isFile()) {
44
+ tovaFiles = srcPath.endsWith('.tova') ? [srcPath] : [];
45
+ } else {
46
+ tovaFiles = findFiles(srcPath, '.tova');
47
+ }
48
+ const srcDir = existsSync(srcPath) && statSync(srcPath).isFile() ? dirname(srcPath) : srcPath;
49
+ if (tovaFiles.length === 0) {
50
+ console.error('No .tova files found');
51
+ process.exit(1);
52
+ }
53
+
54
+ let totalErrors = 0;
55
+ let totalWarnings = 0;
56
+ const seenCodes = new Set();
57
+ let _checkScorecardData = null;
58
+ const _allCheckWarnings = [];
59
+
60
+ for (const file of tovaFiles) {
61
+ const relPath = relative(srcDir, file);
62
+ const start = Date.now();
63
+ try {
64
+ const source = readFileSync(file, 'utf-8');
65
+ const lexer = new Lexer(source, file);
66
+ const tokens = lexer.tokenize();
67
+ const parser = new Parser(tokens, file);
68
+ const ast = parser.parse();
69
+ const analyzer = new Analyzer(ast, file, { strict: checkStrict, strictSecurity: checkStrictSecurity, tolerant: true });
70
+ const result = analyzer.analyze();
71
+
72
+ const errors = result.errors || [];
73
+ const warnings = result.warnings || [];
74
+ totalErrors += errors.length;
75
+ totalWarnings += warnings.length;
76
+ _allCheckWarnings.push(...warnings);
77
+
78
+ // Collect security info for scorecard
79
+ if (!_checkScorecardData) {
80
+ const hasServer = ast.body.some(n => n.type === 'ServerBlock');
81
+ const hasEdge = ast.body.some(n => n.type === 'EdgeBlock');
82
+ if (hasServer || hasEdge) {
83
+ const secNode = ast.body.find(n => n.type === 'SecurityBlock');
84
+ let secCfg = null;
85
+ if (secNode) {
86
+ secCfg = {};
87
+ for (const child of secNode.body || []) {
88
+ if (child.type === 'AuthDeclaration') secCfg.auth = { authType: child.authType || 'jwt', storage: child.config?.storage?.value };
89
+ else if (child.type === 'CsrfDeclaration') secCfg.csrf = { enabled: child.config?.enabled?.value !== false };
90
+ else if (child.type === 'RateLimitDeclaration') secCfg.rateLimit = { max: child.config?.max?.value };
91
+ else if (child.type === 'CspDeclaration') secCfg.csp = { default_src: true };
92
+ else if (child.type === 'CorsDeclaration') {
93
+ const origins = child.config?.origins;
94
+ secCfg.cors = { origins: origins ? (origins.elements || []).map(e => e.value) : [] };
95
+ }
96
+ else if (child.type === 'AuditDeclaration') secCfg.audit = { events: ['auth'] };
97
+ }
98
+ }
99
+ _checkScorecardData = { securityConfig: secCfg, hasServer, hasEdge };
100
+ }
101
+ }
102
+
103
+ if (errors.length > 0 || warnings.length > 0) {
104
+ const formatter = new DiagnosticFormatter(source, file);
105
+ for (const e of errors) {
106
+ console.error(formatter.formatError(e.message, { line: e.line, column: e.column }, { hint: e.hint, code: e.code, length: e.length, fix: e.fix }));
107
+ if (e.code) seenCodes.add(e.code);
108
+ }
109
+ for (const w of warnings) {
110
+ console.warn(formatter.formatWarning(w.message, { line: w.line, column: w.column }, { hint: w.hint, code: w.code, length: w.length, fix: w.fix }));
111
+ if (w.code) seenCodes.add(w.code);
112
+ }
113
+ }
114
+
115
+ if (isVerbose) {
116
+ const elapsed = Date.now() - start;
117
+ console.log(` ✓ ${relPath} (${elapsed}ms)`);
118
+ }
119
+ } catch (err) {
120
+ totalErrors++;
121
+ if (err.errors) {
122
+ const source = readFileSync(file, 'utf-8');
123
+ console.error(richError(source, err, file));
124
+ } else {
125
+ console.error(` ✗ ${relPath}: ${err.message}`);
126
+ }
127
+ }
128
+ }
129
+
130
+ // Security scorecard (shown with --verbose or --strict-security, suppressed with --quiet)
131
+ if ((isVerbose || checkStrictSecurity) && !isQuiet && _checkScorecardData) {
132
+ const scorecard = generateSecurityScorecard(
133
+ _checkScorecardData.securityConfig,
134
+ _allCheckWarnings,
135
+ _checkScorecardData.hasServer,
136
+ _checkScorecardData.hasEdge
137
+ );
138
+ if (scorecard) console.log(scorecard.format());
139
+ }
140
+
141
+ if (!isQuiet) {
142
+ console.log(`\n ${tovaFiles.length} file${tovaFiles.length === 1 ? '' : 's'} checked, ${formatSummary(totalErrors, totalWarnings)}`);
143
+ // Show explain hint for encountered error codes
144
+ if (seenCodes.size > 0 && (totalErrors > 0 || totalWarnings > 0)) {
145
+ const codes = [...seenCodes].sort().slice(0, 5).join(', ');
146
+ const more = seenCodes.size > 5 ? ` and ${seenCodes.size - 5} more` : '';
147
+ console.log(`\n Run \`tova explain <code>\` for details on: ${codes}${more}`);
148
+ }
149
+ console.log('');
150
+ }
151
+ if (totalErrors > 0) process.exit(1);
152
+ }
153
+
154
+ export { checkProject };