From 4b707612b5de9087746baaffb7b1e5c9d575c619 Mon Sep 17 00:00:00 2001 From: Naman Goel Date: Thu, 17 Oct 2024 15:01:27 -0700 Subject: [PATCH] fix: cyclic reference issues in calc parser --- packages/style-value-parser/src/core.d.ts | 49 +++++--- packages/style-value-parser/src/core.js | 106 +++++++++++------- .../src/css-types/{calc-sum.js => calc.js} | 60 ++++++---- .../style-value-parser/src/css-types/index.js | 2 +- 4 files changed, 134 insertions(+), 83 deletions(-) rename packages/style-value-parser/src/css-types/{calc-sum.js => calc.js} (65%) diff --git a/packages/style-value-parser/src/core.d.ts b/packages/style-value-parser/src/core.d.ts index 4a12616e..bb5647bb 100644 --- a/packages/style-value-parser/src/core.d.ts +++ b/packages/style-value-parser/src/core.d.ts @@ -24,10 +24,10 @@ export declare class Parser { static always(output: T): Parser; where(predicate: ($$PARAM_0$$: T) => boolean): Parser; static oneOf(...parsers: ReadonlyArray>): Parser; - static sequence>>( + static sequence>>( ...parsers: T ): ParserSequence; - static setOf>>( + static setOf>>( ...parsers: T ): ParserSet; static zeroOrMore(parser: Parser): ZeroOrMoreParsers; @@ -57,28 +57,41 @@ declare class OneOrMoreParsers extends Parser> { constructor(parser: Parser); separatedBy(separator: Parser): OneOrMoreParsers; } - -type MapParserTuple>> = Ps extends [ - Parser, - ...infer Rest extends Parser[], -] - ? [X, ...MapParserTuple] - : []; - export declare class ParserSequence< - T extends ReadonlyArray>, -> extends Parser> { + T extends ConstrainedTuple>, +> extends Parser> { readonly parsers: T; - constructor(...parsers: T); + constructor(parsers: T); separatedBy(separator: Parser): ParserSequence; } declare class ParserSet< - T extends ReadonlyArray>, -> extends Parser> { + T extends ConstrainedTuple>, +> extends Parser> { readonly parsers: T; - constructor(...parsers: T); + constructor(parsers: T); separatedBy(separator: Parser): ParserSet; } -export type FromParser

> = - P extends Parser ? T : never; +type ConstrainedTuple = + | $ReadOnly<[T]> + | $ReadOnly<[T, T]> + | $ReadOnly<[T, T, T]> + | $ReadOnly<[T, T, T, T]> + | $ReadOnly<[T, T, T, T, T]> + | $ReadOnly<[T, T, T, T, T, T]> + | $ReadOnly<[T, T, T, T, T, T, T]> + | $ReadOnly<[T, T, T, T, T, T, T, T]> + | $ReadOnly<[T, T, T, T, T, T, T, T, T]> + | $ReadOnly<[T, T, T, T, T, T, T, T, T, T]>; + +// prettier-ignore +export type FromParser, Fallback = never> = + | Fallback + | T extends Parser ? V : never; + +type ValuesFromParserTuple< + T extends ConstrainedTuple>, + Fallback = never, +> = { + [Key in keyof T]: FromParser; +}; diff --git a/packages/style-value-parser/src/core.js b/packages/style-value-parser/src/core.js index 6e8ac868..c75cd357 100644 --- a/packages/style-value-parser/src/core.js +++ b/packages/style-value-parser/src/core.js @@ -129,14 +129,16 @@ export class Parser<+T> { } // Variadic Generics: ...T, - static sequence>>( + static sequence>>( ...parsers: T ): ParserSequence { - return new ParserSequence(...parsers); + return new ParserSequence(parsers); } - static setOf>>(...parsers: T): ParserSet { - return new ParserSet(...parsers); + static setOf>>( + ...parsers: T + ): ParserSet { + return new ParserSet(parsers); } static zeroOrMore(parser: Parser): ZeroOrMoreParsers { @@ -165,7 +167,11 @@ export class Parser<+T> { static get quotedString(): Parser { // TODO: Add support for escaped code-points - const doubleQuotes: Parser = Parser.sequence( + const doubleQuotes: Parser = Parser.sequence< + $ReadOnly< + [Parser, Parser<$ReadOnlyArray>, Parser], + >, + >( Parser.string('"'), Parser.zeroOrMore( Parser.oneOf( @@ -175,7 +181,11 @@ export class Parser<+T> { ), ), Parser.string('"'), - ).map(([, chars]) => chars.join('')); + ).map( + ([_openQuote, chars, _closeQuote]: $ReadOnly< + [string, $ReadOnlyArray, string], + >) => chars.join(''), + ); const singleQuotes = Parser.sequence( Parser.string("'"), Parser.zeroOrMore( @@ -378,17 +388,18 @@ class OneOrMoreParsers<+T> extends Parser<$ReadOnlyArray> { } } -export class ParserSequence<+T: $ReadOnlyArray>> extends Parser< - $TupleMap(Parser) => O>, +export class ParserSequence<+T: ConstrainedTuple>> extends Parser< + ValuesFromParserTuple, > { +parsers: T; - constructor(...parsers: T) { - super((input): $TupleMap(Parser) => O> | Error => { + constructor(parsers: T) { + super((input: SubString): ValuesFromParserTuple | Error => { const { startIndex, endIndex } = input; let failed: null | Error = null; - const output: $TupleMap(Parser) => O | Error> = parsers.map( + // $FlowFixMe[incompatible-type] + const output: ValuesFromParserTuple | Error = parsers.map( (parser: Parser): X | Error => { if (failed) { return Error('already failed'); @@ -414,34 +425,28 @@ export class ParserSequence<+T: $ReadOnlyArray>> extends Parser< } separatedBy(separator: Parser): ParserSequence { - // This is UNSAFE. No way to safely map a tuple with Flow. - // $FlowFixMe - const parsers: [...T] = [...this.parsers]; - - for (let i = 1; i < parsers.length; i++) { - const originalParser = parsers[i]; - const modifiedParser = originalParser.prefix( - separator.map(() => undefined), - ); - parsers[i] = modifiedParser; - } + // $FlowFixMe[incompatible-type] + const parsers: T = this.parsers.map( + (originalParser: Parser): Parser => + originalParser.prefix(separator.map(() => undefined)), + ); - return new ParserSequence(...parsers); + return new ParserSequence(parsers); } } // Similar to ParserSequence, but the parsers can occur in any order. -class ParserSet<+T: $ReadOnlyArray>> extends Parser< - $TupleMap(Parser) => O>, +class ParserSet<+T: ConstrainedTuple>> extends Parser< + ValuesFromParserTuple, > { +parsers: T; - constructor(...parsers: T) { - super((input): $TupleMap(Parser) => O> | Error => { + constructor(parsers: T) { + super((input: SubString): ValuesFromParserTuple | Error => { const { startIndex, endIndex } = input; let failed: null | Error = null; - const output = []; + const output: [...ValuesFromParserTuple] = [] as $FlowFixMe; const indices: Set = new Set(); for (let i = 0; i < parsers.length; i++) { @@ -479,26 +484,43 @@ class ParserSet<+T: $ReadOnlyArray>> extends Parser< return failed; } - return output as $TupleMap(Parser) => O> | Error; + return output as ValuesFromParserTuple; }); this.parsers = parsers; } separatedBy(separator: Parser): ParserSet { - // This is UNSAFE. No way to safely map a tuple with Flow. - // $FlowFixMe - const parsers: [...T] = [...this.parsers]; - for (let i = 1; i < parsers.length; i++) { - const originalParser = parsers[i]; - const modifiedParser = originalParser.prefix( - separator.map(() => undefined), - ); - parsers[i] = modifiedParser; - } + // This is correct, but Flow can't map tuples right. + // $FlowFixMe[incompatible-type] + const parsers: T = this.parsers.map( + (originalParser: Parser): Parser => + originalParser.prefix(separator.map(() => undefined)), + ); - return new ParserSet(...(parsers as T)); + return new ParserSet(parsers); } } -export type FromParser> = - P extends Parser ? T : empty; +type ConstrainedTuple<+T> = + | $ReadOnly<[T]> + | $ReadOnly<[T, T]> + | $ReadOnly<[T, T, T]> + | $ReadOnly<[T, T, T, T]> + | $ReadOnly<[T, T, T, T, T]> + | $ReadOnly<[T, T, T, T, T, T]> + | $ReadOnly<[T, T, T, T, T, T, T]> + | $ReadOnly<[T, T, T, T, T, T, T, T]> + | $ReadOnly<[T, T, T, T, T, T, T, T, T]> + | $ReadOnly<[T, T, T, T, T, T, T, T, T, T]>; + +// prettier-ignore +export type FromParser<+T: Parser, Fallback = empty> = + | Fallback + | T extends Parser ? V : empty; + +type ValuesFromParserTuple< + +T: ConstrainedTuple>, + Fallback = empty, +> = { + [Key in keyof T]: FromParser, +}; diff --git a/packages/style-value-parser/src/css-types/calc-sum.js b/packages/style-value-parser/src/css-types/calc.js similarity index 65% rename from packages/style-value-parser/src/css-types/calc-sum.js rename to packages/style-value-parser/src/css-types/calc.js index 094c7d59..fa3d43fb 100644 --- a/packages/style-value-parser/src/css-types/calc-sum.js +++ b/packages/style-value-parser/src/css-types/calc.js @@ -35,12 +35,15 @@ class CalcSum { } static get parse(): Parser { return Parser.sequence( - calcValue, + Calc.parse, Parser.oneOf(Parser.string('+'), Parser.string('-')), - calcValue, + Calc.parse, ) .separatedBy(Parser.whitespace) - .map(([left, operator, right]) => new CalcSum(left, right, operator)); + .map( + ([left, operator, right]) => + new CalcSum(left.value, right.value, operator), + ); } } @@ -66,28 +69,41 @@ class CalcProduct { } static get parse(): Parser { return Parser.sequence( - calcValue, + Calc.parse, Parser.oneOf(Parser.string('*'), Parser.string('/')), - calcValue, + Calc.parse, ) .separatedBy(Parser.whitespace) - .map(([left, operator, right]) => new CalcProduct(left, right, operator)); + .map( + ([left, operator, right]) => + new CalcProduct(left.value, right.value, operator), + ); } } -const calcValueWithoutParens = Parser.oneOf( - Parser.float, - dimension, - Percentage.parse, - calcConstant, - CalcSum.parse, - CalcProduct.parse, -); - -export const calcValue: Parser = Parser.oneOf( - calcValueWithoutParens, - calcValueWithoutParens - .surroundedBy(Parser.whitespace.optional) - .prefix(Parser.string('(')) - .skip(Parser.string(')')), -); +class Calc { + +value: CalcValue; + constructor(value: this['value']) { + this.value = value; + } + toString(): string { + return this.value.toString(); + } + static get parse(): Parser { + const calcValueWithoutParens = Parser.oneOf( + Parser.float, + dimension, + Percentage.parse, + calcConstant, + CalcSum.parse, + CalcProduct.parse, + ); + return Parser.oneOf( + calcValueWithoutParens + .surroundedBy(Parser.whitespace.optional) + .prefix(Parser.string('(')) + .skip(Parser.string(')')), + calcValueWithoutParens, + ).map((value) => new Calc(value)); + } +} diff --git a/packages/style-value-parser/src/css-types/index.js b/packages/style-value-parser/src/css-types/index.js index 3dab0cc6..41379150 100644 --- a/packages/style-value-parser/src/css-types/index.js +++ b/packages/style-value-parser/src/css-types/index.js @@ -13,7 +13,7 @@ export * from './angle'; export * from './basic-shape'; export * from './blend-mode'; export * from './calc-constant'; -export * from './calc-sum'; +export * from './calc'; export * from './color'; export * from './common-types'; export * from './custom-ident';