All files / src/rules/converters template-converter.ts

15.25% Statements 18/118
100% Branches 0/0
0% Functions 0/3
15.25% Lines 18/118

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 1192x 2x 2x 2x 2x 2x 2x 2x 2x           2x 2x 2x 2x 2x 2x 2x 2x                                                                                                                                                               2x                                  
import * as path from 'path';
import * as XLSX from 'xlsx';
import type { ImportableRule, ValidationIssue } from '../import-types';
import type { Severity } from '../../types';
import { t } from '../../i18n/messages';
 
const REQUIRED_HEADERS = ['id', 'severity', 'description', 'message'];
const VALID_SEVERITY = ['error', 'warning', 'info'];
 
function splitList(v: unknown): string[] {
  const s = String(v ?? '').trim();
  if (!s) { return []; }
  return s.split(/[,;、\n]/).map(x => x.trim()).filter(Boolean);
}
 
export interface TemplateParseResult {
  rules: ImportableRule[];
  validRules: ImportableRule[];
  yamlContent: string;
  skippedCount: number;
}
 
export function parseTemplate(srcPath: string): TemplateParseResult {
  const ext = path.extname(srcPath).toLowerCase();
  if (!['.xlsx', '.xls'].includes(ext)) {
    throw new Error(t('import.template.badFormat'));
  }
  let wb: XLSX.WorkBook;
  try { wb = XLSX.readFile(srcPath); }
  catch { throw new Error(t('import.template.badFormat')); }

  const sheet = wb.Sheets[wb.SheetNames[0]];
  const rows = XLSX.utils.sheet_to_json<Record<string, string>>(sheet, { defval: '' });
  if (rows.length === 0) {
    throw new Error(t('import.template.empty'));
  }
  const header = Object.keys(rows[0]).map(k => k.trim().toLowerCase());
  const missing = REQUIRED_HEADERS.filter(h => !header.includes(h));
  if (missing.length > 0) {
    throw new Error(t('import.template.notTemplate', { 0: missing.join(', ') }));
  }

  const totalRows = rows.length;
  const rules: ImportableRule[] = rows
    .map((r, idx) => ({ r, rowNo: idx + 2 }))
    .filter(({ r }) => {
      const id = String(r.id ?? '').trim();
      const description = String(r.description ?? '').trim();
      const message = String(r.message ?? '').trim();
      return id !== '' || description !== '' || message !== '';
    })
    .map(({ r, rowNo }) => {
      const issues: ValidationIssue[] = [];

      const rawId = String(r.id ?? '').trim();
      let id = rawId;
      let idPlaceholder = false;
      if (!rawId) {
        id = `rule-${rowNo}`;
        idPlaceholder = true;
        issues.push({ field: 'id', severity: 'error', message: t('import.idMissing') });
      }

      const sevRaw = String(r.severity ?? '').trim().toLowerCase();
      const originalSeverity = String(r.severity ?? '').trim();
      const severity: Severity = VALID_SEVERITY.includes(sevRaw) ? (sevRaw as Severity) : 'warning';
      if (!VALID_SEVERITY.includes(sevRaw)) {
        const detail = originalSeverity ? `: "${originalSeverity}"` : '';
        issues.push({ field: 'severity', severity: 'warning', message: `${t('import.validationSeverityInvalid')}${detail}` });
      }

      const description = String(r.description ?? '').trim();
      if (!description) {
        issues.push({ field: 'description', severity: 'error', message: 'description 为空' });
      }

      const message = String(r.message ?? '').trim();
      if (!message) {
        issues.push({ field: 'message', severity: 'error', message: 'message 为空' });
      }

      return {
        id,
        severity,
        description,
        message,
        languages: splitList(r.languages),
        excludeLanguages: splitList(r.excludeLanguages),
        rowNumber: rowNo,
        originalSeverity,
        idPlaceholder,
        validationIssues: issues.length > 0 ? issues : undefined,
      };
    });

  const validRules = rules.filter(r => !r.validationIssues);
  const yamlContent = buildYaml(validRules);
  const skippedCount = totalRows - rules.length;

  return { rules, validRules, yamlContent, skippedCount };
}
 
function buildYaml(rules: ImportableRule[]): string {
  const lines: string[] = [];
  for (const r of rules) {
    lines.push(`- id: ${r.id}`);
    lines.push(`  severity: ${r.severity}`);
    lines.push(`  description: ${r.description}`);
    lines.push(`  message: ${r.message}`);
    if (r.languages?.length) {
      lines.push(`  languages: [${r.languages.join(', ')}]`);
    }
    if (r.excludeLanguages?.length) {
      lines.push(`  excludeLanguages: [${r.excludeLanguages.join(', ')}]`);
    }
  }
  return lines.join('\n');
}