diff --git a/docs/curriculum-helpers.md b/docs/curriculum-helpers.md index be72ac4c..15c84f82 100644 --- a/docs/curriculum-helpers.md +++ b/docs/curriculum-helpers.md @@ -70,6 +70,69 @@ let match = match[1]; // "myFunc = arg1 => arg1; console.log();\n // captured, unfortunately" ``` +## typedFunctionRegex + +Given a function name and, optionally, a list of parameters, returns a regex that can be used to match that function declaration in a code block. This version is specifically designed to match Typescript typing. + +```typescript +let regex = typedFunctionRegex("foo", "string", [ + "\\s*bar\\s*:\\s*string", + "\\s*baz\\s*:\\s*string", +]); +regex.test("function foo(bar : string, baz : string) : string{}"); // true +regex.test("function foo(bar, baz, qux){}"); // false +regex.test("foo = (bar : string , baz : string) : string => {}"); // true +``` + +### Options + +- capture: boolean - If true, the regex will capture the function definition, including it's body, otherwise not. Defaults to false. +- includeBody: boolean - If true, the regex will include the function body in the match. Otherwise it will stop at the first bracket. Defaults to true. + +```typescript +let regEx = typedFunctionRegex( + "foo", + "string", + ["\\s*bar\\s*:\\s*string", "\\s*baz\\s*:\\s*\\s*string"], + { capture: true }, +); +let combinedRegEx = concatRegex(/var x = "y"; /, regEx); + +let match = `var x = "y"; +function foo(bar : string , baz : string) : string{}`.match(regex); +match[1]; // "function foo(bar, baz){}" +// i.e. only the function definition is captured +``` + +```typescript +let regEx = typedFunctionRegex( + "foo", + "void", + ["bar\\s*:\\s*string", "baz\\s*:\\s*string"], + { includeBody: false }, +); + +let match = + `function foo(bar:string, baz:string) : void {console.log('ignored')}`.match( + regex, + ); +match[1]; // "function foo(bar, baz){" +``` + +NOTE: capture does not work properly with arrow functions. It will capture text after the function body, too. + +```typescript +let regEx = typedFunctionRegex("myFunc", "void", ["arg1\\s*:\\s*string"], { + capture: true, +}); + +let match = + "myFunc = arg1 : string : void => arg1; console.log();\n // captured, unfortunately".match( + regEx, + ); +match[1]; // "myFunc = arg1 => arg1; console.log();\n // captured, unfortunately" +``` + ## prepTestComponent Renders a React component into a DOM element and returns a Promise containing the DOM element. The arguments are, respectively, the component to render and an (optional) object containing the props to pass to the component. diff --git a/packages/helpers/lib/index.ts b/packages/helpers/lib/index.ts index 0f3d8ad9..a3aa587c 100644 --- a/packages/helpers/lib/index.ts +++ b/packages/helpers/lib/index.ts @@ -249,7 +249,7 @@ export function functionRegex( const normalFunctionName = funcName ? "\\s" + escapeRegExp(funcName) : ""; const arrowFunctionName = funcName - ? `(let|const|var)?\\s?${escapeRegExp(funcName)}\\s*=\\s*` + ? `(let|const|var)?\\s?${escapeRegExp(funcName)}\\s*.*=\\s*` : ""; const body = "[^}]*"; @@ -278,6 +278,59 @@ export function functionRegex( ); } +/** + * Generates a regex string to match a function expressions and declarations + * @param funcName - The name of the function to be matched + * @param paramList - Optional list of parameters to be matched + * @param options - Optional object determining whether to capture the match + * (defaults to non-capturing) and whether to include the body in the match (defaults + * to true) + * Specifically designed for Typescript + */ + +export function typedFunctionRegex( + funcName: string | null, + returnType: string | null, + paramList?: string[] | null, + options?: { capture?: boolean; includeBody?: boolean }, +): RegExp { + const capture = options?.capture ?? false; + const includeBody = options?.includeBody ?? true; + const params = paramList ? paramList.join("\\s*,\\s*") : "[^)]*"; + + const normalReturnType = returnType ? escapeRegExp(returnType) : ""; + + const normalFunctionName = funcName ? "\\s" + escapeRegExp(funcName) : ""; + const arrowFunctionName = funcName + ? `(let|const|var)?\\s?${escapeRegExp(funcName)}\\s*.*=\\s*` + : ""; + const body = "[^}]*"; + + const funcREHead = `function\\s*${normalFunctionName}\\s*\\(\\s*${params}\\s*\\)\\s*:\\s*${escapeRegExp(normalReturnType)}\\s*\\{`; + const funcREBody = `${body}\\}`; + const funcRegEx = includeBody + ? `${funcREHead}${funcREBody}` + : `${funcREHead}`; + + const arrowFuncREHead = `${arrowFunctionName}\\(?\\s*${params}\\s*\\)?\\s*:\\s*${escapeRegExp(normalReturnType)}\\s*=>\\s*\\{?`; + const arrowFuncREBody = `${body}\\}?`; + const arrowFuncRegEx = includeBody + ? `${arrowFuncREHead}${arrowFuncREBody}` + : `${arrowFuncREHead}`; + + const anonymousFunctionName = funcName + ? `(let|const|var)?\\s?${escapeRegExp(funcName)}\\s*=\\s*function\\s*` + : ""; + const anonymousFuncREHead = `${anonymousFunctionName}\\(\\s*${params}\\s*\\)\\s*:\\s*${escapeRegExp(normalReturnType)}\\s*\\{`; + const anonymousFuncRegEx = includeBody + ? `${anonymousFuncREHead}${funcREBody}` + : `${anonymousFuncREHead}`; + + return new RegExp( + `(${capture ? "" : "?:"}${funcRegEx}|${arrowFuncRegEx}|${anonymousFuncRegEx})`, + ); +} + function _permutations(permutation: (string | RegExp)[]) { const permutations: (string | RegExp)[][] = []; diff --git a/packages/tests/curriculum-helper.test.tsx b/packages/tests/curriculum-helper.test.tsx index 304494db..952bf8fe 100644 --- a/packages/tests/curriculum-helper.test.tsx +++ b/packages/tests/curriculum-helper.test.tsx @@ -8,6 +8,7 @@ import htmlTestValues from "./__fixtures__/curriculum-helpers-html"; import jsTestValues from "./__fixtures__/curriculum-helpers-javascript"; import whiteSpaceTestValues from "./__fixtures__/curriculum-helpers-remove-white-space"; import * as helper from "./../helpers/lib/index"; +import { typedFunctionRegex } from "./../helpers/lib/index"; const { stringWithWhiteSpaceChars, stringWithWhiteSpaceCharsRemoved } = whiteSpaceTestValues; @@ -485,6 +486,17 @@ describe("functionRegex", () => { expect(regEx.test("function myFunc(arg1, arg3){}")).toBe(false); }); + it("matches a named function that uses Typescript types", () => { + const funcName = "myFunc"; + const regEx = typedFunctionRegex(funcName, "string", [ + "arg1\\s*:\\s*string", + "arg2\\s*:\\s*string", + ]); + expect( + regEx.test("function myFunc(arg1 : string, arg2 : string) : string{}"), + ).toBe(true); + }); + it("matches arrow functions", () => { const funcName = "myFunc"; const regEx = functionRegex(funcName, ["arg1", "arg2"]); @@ -508,11 +520,31 @@ describe("functionRegex", () => { expect(regEx.test("function(arg1, arg2) {}")).toBe(true); }); + it("matches anonymous Typescript functions", () => { + const regEx = typedFunctionRegex(null, "string", [ + "arg1\\s*:\\s*string", + "arg2\\s*:\\s*string", + ]); + expect( + regEx.test("function(arg1 : string , arg2:string) : string {}"), + ).toBe(true); + }); + it("matches anonymous arrow functions", () => { const regEx = functionRegex(null, ["arg1", "arg2"]); expect(regEx.test("(arg1, arg2) => {}")).toBe(true); }); + it("matches anonymous Typescript arrow functions", () => { + const regEx = typedFunctionRegex(null, "string", [ + "arg1\\s*:\\s*string", + "arg2\\s*:\\s*string", + ]); + expect(regEx.test("(arg1 : string, arg2 : string) : string => {}")).toBe( + true, + ); + }); + it("matches let or const declarations if they are present", () => { const regEx = functionRegex("myFunc", ["arg1", "arg2"]); const match = "let myFunc = (arg1, arg2) => {}".match(regEx); @@ -574,6 +606,19 @@ describe("functionRegex", () => { ); }); + it("matches a arrow function that uses Typescript types", () => { + const funcName = "myFunc"; + const regEx = typedFunctionRegex(funcName, "string", [ + "arg1\\s*:\\s*string", + "arg2\\s*:\\s*string", + ]); + expect( + regEx.test( + "myFunc = (arg1 : string, arg2 : string) : string => {return arg1 + arg2}", + ), + ).toBe(true); + }); + it("can match just up to the opening bracket for an arrow function", () => { const code = `const naomi = (love) => { return love ** 2