remeda
Version:
A utility library for JavaScript and Typescript.
1 lines • 7.79 kB
Source Map (JSON)
{"version":3,"file":"stringToPath.cjs","names":["result: Array<string | number>","match: RegExpExecArray | null"],"sources":["../src/stringToPath.ts"],"sourcesContent":["import type { IsNumericLiteral, IsStringLiteral } from \"type-fest\";\n\n// This is the most efficient way to check an arbitrary string if it is a simple\n// non-negative integer. We use character ranges instead of the `\\d` character\n// class to avoid matching non-ascii digits (e.g. Arabic-Indic digits), while\n// maintaining that the regular expression supports unicode.\nconst NON_NEGATIVE_INTEGER_RE = /^(?:0|[1-9][0-9]*)$/u;\n\ntype StringToPath<S> =\n // We can only compute the path type for literals that TypeScript can\n // break down further into parts.\n IsStringLiteral<S> extends true\n ? StringToPathImpl<S>\n : Array<string | number>;\n\ntype StringToPathImpl<S> =\n // We start by checking the 2 quoted variants of the square bracket access\n // syntax. We do this in a single check and not in a subsequent check that\n // would only extract the quoted part so that we can catch cases where the\n // quoted part itself contains square brackets. This allows TypeScript to be\n // \"greedy\" about what it infers into Quoted and DoubleQuoted.\n S extends `${infer Head}['${infer Quoted}']${infer Tail}`\n ? [...StringToPath<Head>, Quoted, ...StringToPath<Tail>]\n : S extends `${infer Head}[\"${infer DoubleQuoted}\"]${infer Tail}`\n ? [...StringToPath<Head>, DoubleQuoted, ...StringToPath<Tail>]\n : // If we have an unquoted property access we also need to run the\n // contents recursively too (unlike the quoted variants above).\n S extends `${infer Head}[${infer Unquoted}]${infer Tail}`\n ? [\n ...StringToPath<Head>,\n ...StringToPath<Unquoted>,\n ...StringToPath<Tail>,\n ]\n : // Finally, we process any dots one after the other from left to\n // right. TypeScript will be non-greedy here, putting *everything*\n // after the first dot into the Tail.\n S extends `${infer Head}.${infer Tail}`\n ? [...StringToPath<Head>, ...StringToPath<Tail>]\n : // Finally we need to handle the few cases of simple literals.\n \"\" extends S\n ? // There are some edge-cases where Lodash will try to access an\n // empty property, but those seem nonsensical in practice so we\n // prefer just skipping these cases.\n []\n : // We differ from Lodash in the way we handle numbers. Lodash\n // returns everything in the path as a string, and relies on JS to\n // coerce array accessors to numbers (or the other way around in\n // practice, e.g., `myArray[123] === myArray['123']`), but from a\n // typing perspective the two are not the same and we need the\n // path to be accurate about it.\n S extends `${infer N extends number}`\n ? [\n // TypeScript considers \" 123 \" to still extend `${number}`,\n // but would type is as `string` instead of a literal. We\n // can use that fact to make sure we only consider simple\n // number literals as numbers, and take the rest as strings.\n IsNumericLiteral<N> extends true ? N : S,\n ]\n : // This simplest form of a path is just a single string literal.\n [S];\n\n/**\n * A utility to allow JSONPath-like strings to be used in other utilities which\n * take an array of path segments as input (e.g. `prop`, `setPath`, etc...).\n * The main purpose of this utility is to act as a bridge between the runtime\n * implementation that converts the path to an array, and the type-system that\n * parses the path string **type** into an array **type**. This type allows us\n * to return fine-grained types and to enforce correctness at the type-level.\n *\n * We **discourage** using this utility for new code. This utility is for legacy\n * code that already contains path strings (which are accepted by Lodash). We\n * strongly recommend using *path arrays* instead as they provide better\n * developer experience via significantly faster type-checking, fine-grained\n * error messages, and automatic typeahead suggestions for each segment of the\n * path.\n *\n * *There are a bunch of limitations to this utility derived from the\n * limitations of the type itself, these are usually edge-cases around deeply\n * nested paths, escaping, whitespaces, and empty segments. This is true even\n * in cases where the runtime implementation can better handle them, this is\n * intentional. See the tests for this utility for more details and the\n * expected outputs*.\n *\n * @param path - A string path.\n * @signature\n * R.stringToPath(path)\n * @example\n * R.stringToPath('a.b[0].c') // => ['a', 'b', 0, 'c']\n * @dataFirst\n * @category Utility\n */\nexport function stringToPath<const Path extends string>(\n path: Path,\n): StringToPath<Path> {\n const result: Array<string | number> = [];\n\n // There are four possible ways to define a path segment::\n // - `propName`: is used to parse dot-notation paths, e.g. 'foo.bar.baz', we\n // allow multiple sequential dots because our type allows them, but they are\n // semantically meaningless. Note that this would also allow strings starting\n // with a dot, e.g. `.foo`, this is fine because the dot itself is just used\n // as a separator and is ignored in the final path.\n // - `quoted`, `doubleQuoted`: used within square bracket notation to prevent\n // any recursive parsing of their contents. As for the type itself, we only\n // allow quote symbols to appear immediately after the opening square bracket\n // and immediately before the closing square bracket, this allows us to handle\n // more gracefully cases where a quote symbol might appear within the quoted\n // part.\n // - `unquoted`: If the contents of the square brackets are not quoted they\n // will match this group (this is why the order is important here!). Contents\n // of this group get parsed *recursively*.\n //\n // NOTE: We limit all repeats to 4096 characters to avoid a possible attack\n // vector when the input to this function is controlled by the user. This is\n // due to a DoS timing attack because regex backtracking is non-linear.\n // @see: https://codeql.github.com/codeql-query-help/javascript/js-polynomial-redos/\n const pathSegmentRe =\n /\\.{0,4096}(?<propName>[^.[\\]]+)|\\['(?<quoted>.{0,4096}?)'\\]|\\[\"(?<doubleQuoted>.{0,4096}?)\"\\]|\\[(?<unquoted>.{0,4096}?)\\]/uy;\n\n let match: RegExpExecArray | null;\n while ((match = pathSegmentRe.exec(path)) !== null) {\n const { propName, quoted, doubleQuoted, unquoted } = match.groups!;\n\n if (unquoted !== undefined) {\n result.push(...stringToPath(unquoted));\n continue;\n }\n\n result.push(\n propName === undefined\n ? (quoted ?? doubleQuoted!)\n : // The only way to differentiate between array indices and properties\n // is to check if the property is a non-negative integer. In those\n // cases we perform the conversion (unlike Lodash).\n NON_NEGATIVE_INTEGER_RE.test(propName)\n ? Number(propName)\n : propName,\n );\n }\n\n return result;\n}\n"],"mappings":"AAMA,MAAM,EAA0B,uBAqFhC,SAAgB,EACd,EACoB,CACpB,IAAMA,EAAiC,EAAE,CAsBnC,EACJ,8HAEEC,EACJ,MAAQ,EAAQ,EAAc,KAAK,EAAK,IAAM,MAAM,CAClD,GAAM,CAAE,WAAU,SAAQ,eAAc,YAAa,EAAM,OAE3D,GAAI,IAAa,IAAA,GAAW,CAC1B,EAAO,KAAK,GAAG,EAAa,EAAS,CAAC,CACtC,SAGF,EAAO,KACL,IAAa,IAAA,GACR,GAAU,EAIX,EAAwB,KAAK,EAAS,CACpC,OAAO,EAAS,CAChB,EACP,CAGH,OAAO"}