-
-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): add partial support for ES6 computed property names (#293)
* test(core): add empty string and trailing dot paths cases * feat(core): add a partial support for computed property names Computed property names were introduced in ES6. "Partial" in a sense that few computed property name usage are not supported. As well for when using with ES5 functions, not arrow functions. Closes #289 * docs: adjust odd-property name limitations * feat(core): improve `getMembersFromArrowFunctionExpr` to avoid map and match all the away * feat(core): add suport to computed property names for ES5 * docs: remove the ES5 limitation of odd-property name * feat(core): reset the state of the shared regex before Since we are using a constant regex that has the `g` flag in iterator we should make sure that it is in the initial state. See http://speakingjs.com/es5/ch19.html
- Loading branch information
1 parent
34fdc33
commit 93d3f66
Showing
4 changed files
with
306 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 91 additions & 30 deletions
121
packages/core/src/lib/create-mapper/get-member-path.util.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,42 +1,103 @@ | ||
import type { Selector } from '@automapper/types'; | ||
|
||
// TODO: support odd properties like: 'odd-property', 'odd.property'? | ||
/** | ||
* An regular expression to match will all property names of a given cleaned | ||
* arrow function expression. Note that if there is some computed names thus | ||
* they are returning if the quotes. | ||
* | ||
* @example | ||
* ```js | ||
* "s=>s.foo['bar']".match(RE_ARROW_FN_SELECTOR_PROPS) | ||
* // will return | ||
* ["foo", "'bar'"] | ||
* ``` | ||
* | ||
* ### Explanation: | ||
* ``` | ||
* (?: // (begin of non-capturing group) | ||
* (?<= // (begin of positive lookbehind) | ||
* \[ // matches a literal "[" but without including it in the match result | ||
* ) // (end of positive lookbehind) | ||
* ( // (begin capturing group #1) | ||
* ['"] // match one occurrence of a single or double quotes characters | ||
* ) // (end capturing group #1) | ||
* ( // (begin capturing group #2) | ||
* .*? // followed by 0 or more of any character, but match as few characters as possible (which is 0) | ||
* ) // (end capturing group #2) | ||
* \1 // followed by the result of capture group #1 | ||
* ) // (end of non-capturing group) | ||
* | // Or matches with... | ||
* (?: // (begin of non-capturing group) | ||
* (?<= // (begin of positive lookbehind) | ||
* \. // matches a literal "." but without including it in the match result | ||
* ) // (end of positive lookbehind) | ||
* [^.[]+ // followed by 1 or more occurrences of any character but "." nor "[" | ||
* ) // (end of non-capturing group) | ||
* ``` | ||
*/ | ||
const RE_FN_SELECTOR_PROPS = /(?:(?<=\[)(['"])(.*?)\1)|(?:(?<=\.)[^.[]+)/g; | ||
|
||
/** | ||
* For a given cleaned and serialzed JS function selector expression, return a | ||
* list of all members that were selected. | ||
* | ||
* @returns `null` if the given `fnSelector` doesn't match with anything. | ||
*/ | ||
export function getMembers(fnSelectorStr: string): string[] | null { | ||
// Making sure that the shared constant `/g` regex is in its initial state. | ||
RE_FN_SELECTOR_PROPS.lastIndex = 0; | ||
|
||
let matches = RE_FN_SELECTOR_PROPS.exec(fnSelectorStr); | ||
|
||
if (!matches) return null; | ||
|
||
const members: string[] = []; | ||
do { | ||
// Use the value of the second captured group or the entire match, since | ||
// we do not want to capture the matching quotes (when any) | ||
const propFound = matches[2] ?? matches[0]; | ||
// ^^ Using the nullish operator since the left | ||
// side could be an empty string, which is falsy. | ||
members.push(propFound); | ||
} while ((matches = RE_FN_SELECTOR_PROPS.exec(fnSelectorStr))); | ||
|
||
return members; | ||
} | ||
|
||
/** | ||
* Get a dot-separated string of the properties selected by a given `fn` selector | ||
* function. | ||
* | ||
* @example | ||
* ```js | ||
* getMemberPath(s => s.foo.bar) === 'foo.bar' | ||
* getMemberPath(s => s['foo']) === 'foo' | ||
* getMemberPath(s => s.foo['']) === 'foo.' | ||
* // invalid usage | ||
* getMemberPath(s => s) === '' | ||
* ``` | ||
*/ | ||
export function getMemberPath(fn: Selector): string { | ||
const fnString = fn | ||
.toString() | ||
// .replace(/\/\* istanbul ignore next \*\//g, '') | ||
.replace(/cov_.+\n/g, ''); | ||
|
||
// ES6 prop selector: | ||
// "x => x.prop" | ||
if (fnString.includes('=>')) { | ||
const cleaned = cleanseAssertionOperators( | ||
fnString.substring(fnString.indexOf('.') + 1) | ||
); | ||
|
||
if (cleaned.includes('=>')) return ''; | ||
return cleaned; | ||
} | ||
|
||
// ES5 prop selector: | ||
// "function (x) { return x.prop; }" | ||
// webpack production build excludes the spaces and optional trailing semicolon: | ||
// "function(x){return x.prop}" | ||
// FYI - during local dev testing i observed carriage returns after the curly brackets as well | ||
// Note by maintainer: See https://github.com/IRCraziestTaxi/ts-simple-nameof/pull/13#issuecomment-567171802 for | ||
// explanation of this regex. | ||
const matchRegex = /function\s*\(\w+\)\s*{[\r\n\s]*return\s+\w+\.((\w+\.)*(\w+))/i; | ||
|
||
const es5Match = fnString.match(matchRegex); | ||
|
||
if (es5Match) { | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
return es5Match[1]!; | ||
} | ||
|
||
return ''; | ||
const cleaned = cleanseAssertionOperators(fnString); | ||
|
||
// Note that we don't need to remove the `return` keyword because, for instance, | ||
// `(x) => { return x.prop }` will be turn into `x=>returnx.prop` (cleaned) | ||
// thus we'll still be able to get only the string `prop` properly. | ||
// The same for `function (x){}` | ||
const members = getMembers(cleaned); | ||
|
||
return members ? members.join('.') : ''; | ||
} | ||
|
||
/** | ||
* @returns {string} The given `parseName` but without curly brackets, blank | ||
* spaces, semicolons, parentheses, "?" and "!" characters. | ||
*/ | ||
function cleanseAssertionOperators(parsedName: string): string { | ||
return parsedName.replace(/[?!]/g, '').replace(/(?:\s|;|{|}|\(|\)|)+/gm, ''); | ||
return parsedName.replace(/[\s{}()?!;]+/gm, ''); | ||
} |
Oops, something went wrong.