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
63 changes: 63 additions & 0 deletions docs/curriculum-helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
55 changes: 54 additions & 1 deletion packages/helpers/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[^}]*";

Expand Down Expand Up @@ -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)[][] = [];

Expand Down
45 changes: 45 additions & 0 deletions packages/tests/curriculum-helper.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"]);
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Loading