Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions cli/src/osf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class FormulaEvaluator {
private data: SpreadsheetData;
private formulas: Map<string, string>;
private computed: Map<string, CellValue>;
private evaluating: Set<string>; // For circular reference detection
private evaluating: Set<string>; // For circular reference detection; see cli/tests/cli.test.ts

constructor(data: SpreadsheetData, formulas: FormulaDefinition[]) {
this.data = { ...data };
Expand Down Expand Up @@ -175,7 +175,7 @@ class FormulaEvaluator {
getCellValue(row: number, col: number): CellValue {
const key = `${row},${col}`;

// Check for circular reference
// Check for circular reference (validated by cli/tests/cli.test.ts)
if (this.evaluating.has(key)) {
throw new Error(`Circular reference detected at cell ${this.coordsToCellRef(row, col)}`);
}
Expand Down Expand Up @@ -725,6 +725,14 @@ function exportJson(doc: OSFDocument): string {
// Get all computed values including formulas
const allValues = evaluator.getAllComputedValues(maxRow, maxCol);

// Fail if any formula evaluation resulted in an error (e.g., circular references)
const errorCell = Object.values(allValues).find(
v => typeof v === 'string' && v.startsWith('#ERROR:')
);
if (typeof errorCell === 'string') {
throw new Error(errorCell.replace('#ERROR: ', ''));
}

// Convert to array format with computed values
const computedData = Object.entries(allValues).map(([cell, value]) => {
const [r, c] = cell.split(',').map(Number);
Expand Down
33 changes: 33 additions & 0 deletions cli/tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ const FORMULA_TEST_OSF = `@meta {
formula (2,4): "=C1+C2";
}`;

const CIRCULAR_FORMULA_OSF = `@meta {
title: "Circular Formula Test";
}

@sheet {
name: "CycleSheet";
cols: [A, B];
data {
(2,1) = 1;
}
formula (1,1): "=B1";
formula (1,2): "=A1";
}`;

const INVALID_OSF = `@meta {
title: "Unclosed Block"
// Missing closing brace`;
Expand Down Expand Up @@ -377,6 +391,25 @@ describe('OSF CLI', () => {
}
});

it('should fail exporting OSF with circular formulas to JSON', () => {
const cycleFile = join(TEST_FIXTURES_DIR, 'cycle_test.osf');
writeFileSync(cycleFile, CIRCULAR_FORMULA_OSF, 'utf8');

try {
const result = execSync(`node "${CLI_PATH}" export "${cycleFile}" --target json`, {
encoding: 'utf8',
});
expect.fail(`Expected export to fail but succeeded with output: ${result}`);
} catch (err: any) {
const output = (err.stderr || err.stdout) as string;
expect(output).toContain('Circular reference detected');
} finally {
if (existsSync(cycleFile)) {
unlinkSync(cycleFile);
}
}
});

it('should export OSF to file', () => {
execSync(`node "${CLI_PATH}" export "${testFile}" --output "${outputFile}"`, {
encoding: 'utf8',
Expand Down
Loading