Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added

- Added `DetailedCellError.hasMessage`, so a consumer can tell a cell error that never carried a message (e.g. a custom function that omitted one) apart from one with a deliberately empty message — both previously surfaced identically as `message: ''`. [#1547](https://github.com/handsontable/hyperformula/issues/1547)
- Added `DetailedCellError.originFunction` and `DetailedCellError.argumentIndex`, so a consumer can tell which function or operator produced a cell error and, when it failed coercing one of its own arguments, which argument that was. First occurrence wins: a function or operator that only reads or propagates an error never claims to have produced it. [#1547](https://github.com/handsontable/hyperformula/issues/1547)

### Changed

Expand Down
8 changes: 7 additions & 1 deletion src/Cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,16 @@ export class CellError {
* propagated error never acquires one: the reading function's own argument slot is not the
* offending argument.
*
* Also a no-op once `originFunction` is set, even for a non-propagated error: a nested call's
* own error (e.g. `SQRT(-1)` inside `=DATE(1,1,SQRT(-1))`) stamps its origin before the outer
* call's coercion loop ever sees it, and that loop's own argument slot is not the one that
* actually produced the error — attaching an index here would pair someone else's origin with
* this call's argument position, which is incoherent.
*
* @param {number} index - zero-based index of the offending argument
*/
public withArgumentIndex(index: number): CellError {
if (this.propagated || this.argumentIndex !== undefined) {
if (this.propagated || this.originFunction !== undefined || this.argumentIndex !== undefined) {
return this
}
return new CellError(this.type, this.message, this.root, this.originFunction, index, this.propagated, this.originAddress, this.originAddressVersion)
Expand Down
33 changes: 33 additions & 0 deletions src/CellValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,37 @@ export class DetailedCellError {
*/
public readonly hasMessage: boolean

/**
* What produced this error.
*
* Usually the function or operator that rejected a value, e.g. `'SUM'` or `'divide'`.
* Errors that exist before any function reads them name what built them instead:
* `'reference'` for a reference that cannot be resolved, `'removed reference'` for one
* destroyed by removing rows or columns, `'parser'` for a formula that could not be
* parsed, `'user input'` for an error value typed into a cell, and `'literal'` for one
* written into a formula.
*
* First occurrence wins, so a function that only read the error never replaces that:
* `=SUM(SQRT(-1))` reports `'SQRT'`, and `=SUM(A2:A99999999999)` reports `'reference'`.
*
* `undefined` when nothing produced the error in this sense — `#SPILL!` and `#CYCLE!`
* arise from the layout of a sheet rather than from evaluating a value.
*/
public readonly originFunction?: string

/**
* The zero-based index of the argument that was rejected, when `originFunction` names a
* function whose own argument coercion produced this error.
*
* `undefined` whenever the error was not a coercion failure on one of the origin
* function's own arguments — including when it came from a nested call (its own
* `originFunction` already claimed it), when it was propagated from elsewhere, or when
* the argument was a reference that could not be resolved, which the reference itself
* reports. When a function is applied across an array, an element that fails coercion
* carries the index alongside the function's own name, like any other coercion failure.
*/
public readonly argumentIndex?: number

constructor(
error: CellError,
public readonly value: string,
Expand All @@ -32,6 +63,8 @@ export class DetailedCellError {
this.type = error.type
this.message = error.message ?? ''
this.hasMessage = error.message !== undefined
this.originFunction = error.originFunction
this.argumentIndex = error.argumentIndex
}

public toString(): string {
Expand Down
4 changes: 2 additions & 2 deletions src/interpreter/plugin/FunctionPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,11 +485,11 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck<Function
const coercedValue = this.coerceToType(argumentValue, argumentMetadata, state)

if (coercedValue === undefined && !argumentIgnorableFlags[i]) {
return new CellError(ErrorType.VALUE, ErrorMessage.WrongType)
return new CellError(ErrorType.VALUE, ErrorMessage.WrongType).withArgumentIndex(i)
}

if (coercedValue instanceof CellError && argumentMetadata.argumentType !== FunctionArgumentType.SCALAR) {
return coercedValue
return coercedValue.withArgumentIndex(i)
}

coercedArguments.push(coercedValue)
Expand Down
Loading