Skip to content

Use the Curriculum Helpers

The test runner exposes helpers through __helpers for testing campers’ code.

Use __helpers.removeWhiteSpace(codeString) to remove all whitespace from code:

const code = 'function test() {\n return true;\n}';
__helpers.removeWhiteSpace(code); // "functiontest(){returntrue;}"

This removes whitespace inside string literals too. For example, 'hello world' becomes 'helloworld'.

await __helpers.retryingTest(callback, message, numberOfTries);

The callback must synchronously return a truthy value when the condition is met. Otherwise, the helper schedules another attempt with a 1 ms delay. The optional numberOfTries defaults to 20. After all attempts fail, the returned promise rejects with an error containing message.

For example, wait for an element that is added after an asynchronous operation:

await __helpers.retryingTest(
() => document.querySelector('.result'),
'The result element was not found.',
20
);

Do not pass an async callback: its promise is truthy, so the helper considers the condition met without awaiting its result. Browser scheduling can make the delay longer than 1 ms.

These helpers build text patterns. Prefer the language-specific helpers below when testing code structure.

Available via __helpers.permutateRegex(arrayOfRegexOrStrings, options), this method creates a regex that matches elements in any order by generating all possible permutations.

Use a small, nonempty input: n elements produce n! permutations, including duplicates when elements repeat. An empty input produces a pattern that matches an empty string. The helper uses each regex’s source and discards its flags. Strings are also treated as regex patterns; use escapeRegExp for literal text containing regex metacharacters.

const source1 = 'a';
const regex1 = /b/;
const source2 = 'c';
__helpers.permutateRegex([source1, regex1, source2]).source ===
new RegExp(
/(?:a\s*\|\|\s*b\s*\|\|\s*c|b\s*\|\|\s*a\s*\|\|\s*c|c\s*\|\|\s*a\s*\|\|\s*b|a\s*\|\|\s*c\s*\|\|\s*b|b\s*\|\|\s*c\s*\|\|\s*a|c\s*\|\|\s*b\s*\|\|\s*a)/
).source; // true

The method supports named capturing groups and backreferences:

const regex = __helpers.permutateRegex(['a', /(?<ref>'|"|`)b\k<ref>/], {
elementsSeparator: String.raw`\s*===\s*`
});
regex.source ===
new RegExp(
/(?:a\s*===\s*(?<ref_0>'|"|`)b\k<ref_0>|(?<ref_1>'|"|`)b\k<ref_1>\s*===\s*a)/
).source; // true
regex.test('a === "b"'); // true
regex.test("'b' === a"); // true
regex.test('a === `b`'); // true
regex.test(`a === 'b"`); // false
  • capture: boolean - Whole regex is wrapped in regex group. If capture is true the group will be capturing, otherwise it will be non-capturing. Defaults to false.
  • elementsSeparator: string - Separates permuted elements within single permutation. Defaults to \s*\|\|\s*.
  • permutationsSeparator: string - Separates permutations. Defaults to |.
__helpers.permutateRegex(['a', /b/, 'c'], { capture: true }).source ===
new RegExp(
/(a\s*\|\|\s*b\s*\|\|\s*c|b\s*\|\|\s*a\s*\|\|\s*c|c\s*\|\|\s*a\s*\|\|\s*b|a\s*\|\|\s*c\s*\|\|\s*b|b\s*\|\|\s*c\s*\|\|\s*a|c\s*\|\|\s*b\s*\|\|\s*a)/
).source; // true
__helpers.permutateRegex(['a', /b/, 'c'], { elementsSeparator: ',' }).source ===
new RegExp(/(?:a,b,c|b,a,c|c,a,b|a,c,b|b,c,a|c,b,a)/).source; // true
__helpers.permutateRegex(['a', /b/, 'c'], { permutationsSeparator: '&' })
.source ===
new RegExp(
/(?:a\s*\|\|\s*b\s*\|\|\s*c&b\s*\|\|\s*a\s*\|\|\s*c&c\s*\|\|\s*a\s*\|\|\s*b&a\s*\|\|\s*c\s*\|\|\s*b&b\s*\|\|\s*c\s*\|\|\s*a&c\s*\|\|\s*b\s*\|\|\s*a)/
).source; // true

Available via __helpers.concatRegex(regexOrString1, regexOrString2, ...), this method combines and returns a compiled regex when given two or more regexes or strings.

Input regex flags are not preserved. Strings are treated as regex patterns, not escaped literal text. The combined pattern does not parse code or handle nested structures for you.

For example, if .concatRegex is called with .* and b\s the resulting regex will be equivalent to .*b\s.

const regex1 = /a\s/;
const regex2 = /b/;
__helpers.concatRegex(regex1, regex2).source === 'a\\sb'; // true

Available via __helpers.escapeRegExp(string), this method escapes special regular expression characters in a string.

__helpers.escapeRegExp('hello.world'); // "hello\\.world"
__helpers.escapeRegExp('test[123]'); // "test\\[123\\]"

Use this for literal text that will be inserted into a regex. It does not parse code or make a pattern capable of matching balanced braces.

Use __helpers.removeHtmlComments(codeString) to remove HTML comments (<!-- -->) from code:

const htmlCode = '<div>Hello <!-- this is a comment --> World</div>';
__helpers.removeHtmlComments(htmlCode); // "<div>Hello World</div>"

This is a text replacement, not an HTML parser. It also removes comment-like text inside attribute values. For example, '<div title="<!-- note -->">' becomes '<div title="">'. Use the DOM to inspect elements and attributes.

To instantiate the helper within a test block, use this:

const helper = new __helpers.CSSHelp(document);

In that example, the document variable refers to the document object of the test runner’s iframe.

CSSHelp reads authored rules from the stylesheet selected by getStyleSheet(), not computed styles or every stylesheet in the document. Selector lookups match the serialized selector text exactly; getStyle('p') does not match a rule whose selector is p, div. Ordinary style lookups inspect top-level rules only. Use the media-query helpers to inspect rules inside @media.

There are a few methods available for parsing and testing CSS.

The .getStyle() method takes a CSS selector and returns the first matching CSS style declaration object, or null if no rule matches.

For example, if the camper has written the following CSS:

body {
background-color: rgb(118, 201, 255);
width: 100%;
}

There are three ways to retrieve CSS values from the returned object:

MethodExampleReturns
Direct property accesshelper.getStyle('body')?.width'100%'
Native .getPropertyValue()helper.getStyle('body')?.getPropertyValue('width')'100%'
Helper .getPropVal()helper.getStyle('body')?.getPropVal('width')'100%'

.getPropertyValue() is a native CSS style declaration object method for reading CSS properties by their kebab-case name, such as background-color. See MDN: CSSStyleDeclaration.getPropertyValue().

The .getStyle() helper attaches .getPropVal() to the declaration. It wraps getPropertyValue() and accepts an optional second argument that removes all whitespace when set to true:

const style = helper.getStyle('body');
assert.exists(style);
assert.strictEqual(style.getPropVal('background-color'), 'rgb(118, 201, 255)');
assert.strictEqual(
style.getPropVal('background-color', true),
'rgb(118,201,255)'
);

Whitespace stripping also changes quoted values. A property that is not set in the declaration returns an empty string; inherited or computed values are not supplied. Gradient colors belong to background-image, not background-color.

The .getStyleAny() method takes one array of selectors and returns the first matching style declaration in that array’s order, or null if none match. It does not merge declarations or choose the rule that wins in the cascade.

For example, if the camper has written the following CSS:

.sky {
background-color: #ffcf33;
width: 100%;
}

There are three ways to retrieve CSS values from the returned object:

MethodExampleReturns
Direct property accesshelper.getStyleAny(['.earth', '.sky'])?.width'100%'
Native .getPropertyValue()helper.getStyleAny(['.earth', '.sky'])?.getPropertyValue('width')'100%'
Helper .getPropVal()helper.getStyleAny(['.earth', '.sky'])?.getPropVal('width')'100%'

The returned declaration includes the same .getPropVal() method as .getStyle():

const style = helper.getStyleAny(['.earth', '.sky']);
assert.exists(style);
assert.strictEqual(style.getPropVal('background-color'), 'rgb(255, 207, 51)');
assert.strictEqual(
style.getPropVal('background-color', true),
'rgb(255,207,51)'
);

The .getCSSRules() method takes an at-rule type from media, fontface, import, or keyframes, and returns an array of matching top-level CSS rules. With no argument, it returns all top-level rules. It returns an empty array when there are no matching rules.

For example, if the camper has written the following CSS:

@media (max-width: 100px) {
body {
background-color: green;
}
}

Find the media rule by its condition, then inspect its nested rules:

const mediaRule = helper
.getCSSRules('media')
.find(rule => rule.conditionText === '(max-width: 100px)');
assert.exists(mediaRule);
const bodyRule = Array.from(mediaRule.cssRules).find(
rule => rule.selectorText === 'body'
);
assert.exists(bodyRule);
assert.strictEqual(bodyRule.style.backgroundColor, 'green');

A media rule has a cssRules collection, not a style property. The helper does not recursively search nested at-rules.

The .getRuleListsWithinMedia() method takes exact serialized media text, such as '(max-width: 200px)', and returns an array of rules from the first matching top-level media query. It returns an empty array if none match.

The return result is the equivalent of that media rule’s cssRules property from the return value of .getCSSRules("media").

These methods are not as commonly used, but are available if needed.

The .getStyleDeclarations() method takes a CSS selector and returns an array of all matching top-level CSS style declarations, or an empty array if none match. Unlike .getStyle(), it does not attach .getPropVal() to the declarations; use the native .getPropertyValue() method.

The .isPropertyUsed() method takes a CSS property and checks whether it has a nonempty value in any top-level style rule in the selected stylesheet. It does not inspect declarations inside media queries or other nested rules.

The .getStyleRule() method takes a CSS selector and returns an extended rule object with an .isDeclaredAfter() method, or null if no rule matches. It is not the style declaration returned by .getStyle(). .isDeclaredAfter(selector) compares the positions of the first matching top-level rules in the stylesheet. It returns false if the other selector is absent and does not compare specificity or computed styles.

The .getStyleSheet() method returns the selected CSSStyleSheet, or null if none is found. It checks a linked stylesheet whose URL contains styles and has rules, then style.fcc-injected-styles, then a style element without class or media attributes. It does not combine multiple stylesheets.

The .selectorsFromSelector() method generates tag-based selectors from matching elements and their ancestors. It returns an empty array when no elements match. It does not enumerate every equivalent selector or use element IDs and classes, so do not treat the result as a complete list of valid selectors.

For example if a is passed in a selector, it will return an array filled with several alternate selectors based on what’s nearby. The selectors could include a, label > a , label a , form label a and body form label a.

Use __helpers.removeCssComments(codeString) to remove CSS-style comments (/* */) from code:

const cssCode = 'body { color: red; /* this is a comment */ }';
__helpers.removeCssComments(cssCode); // "body { color: red; }"

This is a regex replacement, not a CSS parser. For example, it changes content: "/* text */" to content: "". Empty comments such as /**/ are not removed. Prefer CSSHelp when checking declarations.

Prefer Explorer when testing JavaScript code structure. It parses nested blocks, strings, and template literals instead of trying to identify them with regex.

The Explorer class is documented under TypeScript Helpers, where it is used to test TypeScript challenges. Many of its helpers, however, are not about TypeScript-specific features: they inspect language constructs that also exist in vanilla JavaScript, such as variables, functions, and classes. Those helpers can be used to test JavaScript challenges too. They are:

These mock methods that can prove to be unpredictable for testing purposes.

Available via __helpers.RandomMocker, this class mocks Math.random for testing purposes. Each time mock() is called the pseudo-random number generator is reset to its initial state, so that the same sequence of random numbers is generated each time. restore() restores the Math.random function saved when the mocker was constructed.

const randomMocker = new __helpers.RandomMocker();
randomMocker.mock();
try {
Math.random(); // first call is always 0.2523451747838408
Math.random(); // second call is always 0.08812504541128874
randomMocker.mock();
Math.random(); // generator is reset, so we get 0.2523451747838408 again
} finally {
randomMocker.restore();
}

The replacement affects every Math.random() call in the test environment while active. It does not affect a reference to the original function saved before mock() was called.

Available via __helpers.spyOn, this function wraps an object’s property in a spy object that records calls and returns.

The spy’s calls property is an array of arrays, each containing the arguments passed to the method in the order it was called.

The spy’s returns property is an array of the return values of the method.

Call restore() on the spy object to restore the original method.

const obj = { method: (arg1, arg2) => `called by: ${arg1} + ${arg2}` };
const spy = __helpers.spyOn(obj, 'method');
try {
obj.method('arg', 'another arg');
obj.method('2nd call');
spy.calls; // [["arg", "another arg"], ["2nd call"]]
spy.returns; // ["called by: arg + another arg", "called by: 2nd call + undefined"]
} finally {
spy.restore();
}

Because the spy calls the original method with .apply(this, args), this is preserved. This lets you spy on a shared prototype method to detect that an instance method was used without knowing the variable’s name. For example, to check that .map() was called on an array:

const spy = __helpers.spyOn(Array.prototype, 'map');
try {
const double = value => value * 2;
const result = [1, 2].map(double);
assert.strictEqual(spy.calls[0][0], double);
assert.deepEqual(spy.returns[0], [2, 4]);
assert.deepEqual(result, [2, 4]);
} finally {
spy.restore();
}

Do not use spyOn(Array.prototype, 'push'). The spy records calls using calls.push(args), so replacing the prototype’s push causes infinite recursion and a stack overflow. Spying on one known array’s push works because the spy’s internal arrays retain their original method:

const arr = [1, 2];
const spy = __helpers.spyOn(arr, 'push');
try {
arr.push(3);
assert.deepEqual(spy.calls, [[3]]);
assert.deepEqual(spy.returns, [3]);
assert.deepEqual(arr, [1, 2, 3]);
} finally {
spy.restore();
}

The spy records calls made through the replaced property while it is active. It does not intercept previously saved references to the original method, record the receiving instance, or identify which source variable was used. A prototype spy can also observe unrelated calls from other arrays in the same environment. Return values are recorded as returned; promises are not awaited, and thrown errors are not added to returns.

Available via __helpers.spyOnCallbacks, this function wraps a method in a spy object that records calls to callbacks passed to that method.

The spy’s callbackSpies property is an array of arrays, where the inner arrays contain spy objects. One for each callback passed to the method. Each time the method is called, a new array of spies is created. i.e. the length of the callbackSpies array is equal to the number of times the spied on method was called.

The spies are either the original argument (if it was not a function) or a spy object with calls and returns properties. The calls is populated with the arguments passed to that callback and the returns with the return values of that callback.

const obj = {
method: (callback, value) => {
return arg1 => {
return callback(arg1, value);
};
}
};
const spy = __helpers.spyOnCallbacks(obj, 'method');
try {
const cb = (arg1, arg2) => {
return `callback called with: ${arg1} + ${arg2}`;
};
const func = obj.method(cb, 'test');
func('one');
func('two');
obj.method(cb, 'test2')('another one');
spy.callbackSpies[0][0].calls; // [["one", "test"], ["two", "test"]]
spy.callbackSpies[0][0].returns; // ["callback called with: one + test", "callback called with: two + test"]
spy.callbackSpies[1][0].calls; // [["another one", "test2"]]
spy.callbackSpies[1][0].returns; // ["callback called with: another one + test2"]
} finally {
spy.restore();
}

Unlike spyOn, spyOnCallbacks does not preserve the receiver when calling the original method or its callbacks. Do not use it when either depends on this. Callback return values are recorded without awaiting promises. Restoring the method does not undo callback wrappers that the method has already retained.

These helpers match text rather than parse JavaScript structure. For AST-based alternatives, see the Explorer class.

Use __helpers.removeJSComments(codeString) to remove JavaScript comments (// and /* */) from code:

const jsCode = 'console.log("hello"); // this is a comment';
__helpers.removeJSComments(jsCode); // 'console.log("hello"); '

This removes comments without normalizing string quotes or providing an AST. If the comment parser throws, the helper returns the original string, so do not use it as a guarantee that every comment was removed. Prefer Explorer for structural checks.

Available via __helpers.isCalledWithNoArgs(functionName, codeString), this method looks for a call with no arguments after removing JavaScript comments.

It only matches calls at the start of a line, allowing leading whitespace, with no whitespace between the name and (. It is not a general search for calls inside expressions or blocks.

const code = 'console.log(); alert("test");';
__helpers.isCalledWithNoArgs('console.log', code); // true
__helpers.isCalledWithNoArgs('alert', code); // false
__helpers.isCalledWithNoArgs('console.log', 'const x = 1; console.log();'); // false
__helpers.isCalledWithNoArgs('console.log', 'console.log ();'); // false

Use a spy when the requirement is that a method is called during execution. This helper only checks source text and cannot prove that a matching call executes.

Available via __helpers.functionRegex(functionName, parameterArray, options), this method returns a regex that can be used to match function declarations in a code block given a function name and, optionally, a list of parameters.

Prefer Explorer.functions or Explorer.allFunctions to inspect functions. This regex helper does not parse nested blocks, string literals, or template literals.

const regex = __helpers.functionRegex('foo', ['bar', 'baz']);
regex.test('function foo(bar, baz){}'); // true
regex.test('function foo(bar, baz, qux){}'); // false
regex.test('foo = (bar, baz) => {}'); // true

Options:

  • capture: boolean - If true, the regex will capture the function definition, including its 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.
const regEx = __helpers.functionRegex('foo', ['bar', 'baz'], { capture: true });
const combinedRegEx = __helpers.concatRegex(/var x = "y";\s*/, regEx);
const match = `var x = "y";
function foo(bar, baz){}`.match(combinedRegEx);
match[1]; // "function foo(bar, baz){}"
// i.e. only the function definition is captured
const regEx = __helpers.functionRegex('foo', ['bar', 'baz'], {
includeBody: false
});
const match = `function foo(bar, baz){console.log('ignored')}`.match(regEx);
match[0]; // "function foo(bar, baz){"

For an arrow function with an expression body, the match can continue into statements after the function:

const regEx = __helpers.functionRegex('myFunc', ['arg1'], { capture: true });
const match =
'myFunc = arg1 => arg1; console.log();\n // captured, unfortunately'.match(
regEx
);
match[1]; // "myFunc = arg1 => arg1; console.log();\n // captured, unfortunately"

With includeBody: true, all supported function forms stop at the first } in the body. That brace can belong to a nested block, an object literal, a string, or template interpolation such as ${value}. Setting capture: true exposes the truncated match; it does not make the body extraction complete.

const regEx = __helpers.functionRegex('registerUser', [], { capture: true });
const match = `function registerUser() {
if (name.trim() === "") {
throw new Error("Name is required");
}
if (age.trim() === "") {
throw new Error("Age is required");
}
}`.match(regEx);
match[1].includes('throw new Error("Name is required")'); // true
match[1].includes('age.trim'); // false

Use Explorer to find the function and inspect its structure instead. This example checks a return containing template interpolation after a nested block:

const explorer = await __helpers.Explorer(`
function describe(value) {
if (value) {
console.log(value);
}
return \`Value: \${value}\`;
}
`);
const fn = explorer.functions.describe;
assert.exists(fn);
assert.isTrue(fn.hasReturn('`Value: ${value}`'));

includeBody: false avoids body extraction, but still uses regex to identify the function header. It does not handle every parameter syntax or distinguish code from text inside comments and strings.

Available via __helpers.getFunctionParams(codeString), this method extracts parameter information from a function definition string.

const funcString = 'function test(a, b = "default", c) { return a + b + c; }';
const params = __helpers.getFunctionParams(funcString);
// params = [
// { name: 'a', defaultValue: undefined },
// { name: 'b', defaultValue: 'default' },
// { name: 'c', defaultValue: undefined }
// ]

This helper splits parameter text on commas and = characters, strips destructuring brackets and braces, and removes quote characters from default values. It does not parse nested defaults, destructuring, or strings containing those separators. For example, a default value of "a,b" is split as though it were two parameters. Prefer Explorer.parameters and inspect each parameter’s value when checking defaults.

Explorer provides Abstract Syntax Tree (AST) analysis for TypeScript and JavaScript. The shared methods and TypeScript-specific methods are documented together below.

Explorer is a chainable class that allows you to call methods on the result of parsing a string. Here’s how to create an instance of Explorer that parses the camper’s code:

const explorer = await __helpers.Explorer(code);

To access a specific statement within the code you need to chain method calls until you reach the desired scope. For example, if the camper has written the following code:

class Spam {
get42(): number {
return 42;
}
}

To check the return type annotation you would write:

const explorer = await __helpers.Explorer(code);
const method = explorer.classes.Spam?.methods.get42;
assert.exists(method);
assert.isTrue(method.hasReturnAnnotation('number'));

Named lookups such as classes.Spam can return undefined, while other operations return an empty Explorer or null when no node is available. Searches inspect the current scope; drill into functions or classes to inspect their members. Explorer does not execute source code or evaluate expressions.

Returns true if the Explorer instance has no AST node, otherwise returns false.

const explorer = await __helpers.Explorer('const a = 1;');
explorer.isEmpty(); // false

Returns the source code representation of the current node, or "no ast" if the node is empty.

const explorer = await __helpers.Explorer('const a = 1;');
explorer.toString(); // "const a = 1;"

Compares the current tree with another tree or string, ignoring semicolons, comments, and irrelevant whitespace differences.

const explorer = await __helpers.Explorer('const a = 1;');
explorer.matches('const a = 1'); // true (ignores whitespace and semicolons)
explorer.matches('const b = 1;'); // false

Everything else is part of the tree and has to be identical, including parentheses. Redundant parentheses, and the optional parentheses around a single arrow function parameter, are all significant:

const explorer = await __helpers.Explorer('const f = (x) => x;');
explorer.matches('const f = (x) => x'); // true
explorer.matches('const f = x => x'); // false
const another = await __helpers.Explorer('const a = (1);');
another.matches('const a = 1'); // false

String literals are compared by value, so the quote character does not matter. A template literal is a different kind of node, though, and never matches a plain string:

const explorer = await __helpers.Explorer("const a = 'hello';");
explorer.matches('const a = "hello"'); // true
explorer.matches('const a = `hello`'); // false

Because of this, matches() is a strict check on one exact way of writing an expression. Whenever a step allows more than one valid form, compare against every form, or drill into the tree and assert on the parts that matter:

const validForms = [
'items.find((item) => item.id === id)',
'items.find(item => item.id === id)',
'items.find((item) => { return item.id === id; })',
'items.find(item => { return item.id === id; })'
];
assert.isTrue(validForms.some(form => found.value.matches(form)));

Note that a list like this still ties campers to the parameter name item and to an arrow function, so prefer inspecting the tree when the step does not require either.

Returns an object mapping variable names to Explorer instances for variable statements in the current scope. It is not a recursive search of nested scopes.

Only the first declaration in each statement is exposed. For example, const a = 1, b = 2; produces an entry for a, but not b. Destructuring declarations are not supported: const { x, y } = obj; produces an entry keyed by "undefined", rather than entries for x and y.

const explorer = await __helpers.Explorer(
'var a = 1; let b = 2; const c = () => {}'
);
const { a, b, c } = explorer.variables; // { a: Explorer, b: Explorer, c: Explorer }
a.matches('var a = 1;'); // true
b.matches('let b = 2;'); // true
c.matches('const c = () => {}'); // true

Returns an Explorer instance representing the assigned value of a variable, property, parameter, or property assignment. Returns an empty Explorer if there is no initializer.

const explorer = await __helpers.Explorer(
'const a = 1; const b = { x: 10 }; const c = a + 1;'
);
const { a, b, c } = explorer.variables;
a.value.toString(); // "1"
b.value.matches('{ x: 10 }'); // true
c.value.toString(); // "a + 1"

Returns an object mapping property names to Explorer instances for explicit property assignments with identifier names in an object literal. Shorthand properties, quoted or computed keys, methods, and spreads are not included. For example, { x, "y": 2, z: 3 } produces only an entry for z.

const explorer = await __helpers.Explorer(
"const obj = { x: 1, y: 'hello', z: true };"
);
const { obj } = explorer.variables;
const props = obj.value.objectProps; // { x: Explorer, y: Explorer, z: Explorer }

functions returns function declarations only. allFunctions also includes arrow functions and function expressions assigned to variables.

const explorer = await __helpers.Explorer(
'function foo() { return 42; } const bar = () => 24;'
);
explorer.functions; // { foo: Explorer }
explorer.allFunctions; // { foo: Explorer, bar: Explorer }

Returns an array of Explorer instances representing the parameters of a function or method.

const explorer = await __helpers.Explorer(
'function foo(x: number, y: string) { return 42; }'
);
const parameters = explorer.functions.foo.parameters;
parameters[0].matches('x: number'); // true
parameters[1].matches('y: string'); // true

Returns an Explorer instance representing the type annotation of the current node, or an empty Explorer if none exists.

const explorer = await __helpers.Explorer(
'const a: number = 1; const b: { age: number } = { age: 33 }'
);
const { a, b } = explorer.variables;
a.annotation; // Explorer with "number"
b.annotation; // Explorer with "{ age: number }"

Returns true if the current node has a type annotation matching the specified string.

const explorer = await __helpers.Explorer('const a: number = 1;');
const { a } = explorer.variables;
a.hasAnnotation('number'); // true
a.hasAnnotation('string'); // false

Returns true if the function/method has a return type annotation matching the specified type.

const explorer = await __helpers.Explorer(
'function foo(): number { return 42; }'
);
const { foo } = explorer.functions;
foo.hasReturnAnnotation('number'); // true
foo.hasReturnAnnotation('string'); // false

Checks whether a function, method, or function-valued variable has a matching top-level return expression.

It does not search returns inside nested if statements or loops, and it does not prove that the return executes.

const explorer = await __helpers.Explorer('function foo() { return 42; }');
const { foo } = explorer.functions;
foo.hasReturn('42'); // true

Returns an object mapping type alias names to Explorer instances for all type aliases in the current scope.

const explorer = await __helpers.Explorer(
'type Foo = { x: number; }; type Bar = { y: string; };'
);
const { types } = explorer; // { Foo: Explorer, Bar: Explorer }
types.Foo.matches('type Foo = { x: number; };'); // true
types.Bar.matches('type Bar = { y: string; };'); // true

Returns an object mapping interface names to Explorer instances for all interfaces in the current scope.

const explorer = await __helpers.Explorer(
'interface Foo { x: number; } interface Bar { y: string; }'
);
const { interfaces } = explorer; // { Foo: Explorer, Bar: Explorer }
interfaces.Foo.matches('interface Foo { x: number; }'); // true
interfaces.Bar.matches('interface Bar { y: string; }'); // true

Returns an object mapping class names to Explorer instances for all classes in the current scope.

const explorer = await __helpers.Explorer(
'class Foo { x: number; } class Bar { y: string; }'
);
const { classes } = explorer; // { Foo: Explorer, Bar: Explorer }
classes.Foo.matches('class Foo { x: number; }'); // true
classes.Bar.matches('class Bar { y: string; }'); // true

Returns an object mapping method names to Explorer instances for all methods in the current class.

const explorer = await __helpers.Explorer(
'class Foo { method1() {} method2() {} }'
);
const { Foo } = explorer.classes;
const { method1, method2 } = Foo.methods; // { method1: Explorer, method2: Explorer }

classConstructor, classProps, and constructorProps

Section titled “classConstructor, classProps, and constructorProps”

Use these to inspect class constructors and class-related properties.

const explorer = await __helpers.Explorer(
'class Foo { prop1: number; constructor(x) { this.y = x; } }'
);
const { Foo } = explorer.classes;
Foo.classConstructor?.matches('constructor(x) { this.y = x; }'); // true
Foo.classProps; // { prop1: Explorer }
Foo.constructorProps; // { y: Explorer }

Returns an object mapping property names to Explorer instances for all properties in a type, interface, or type literal.

const explorer = await __helpers.Explorer(
'type Foo = { x: number; y: string; };'
);
const { Foo } = explorer.types;
const { x, y } = Foo.typeProps; // { x: Explorer, y: Explorer }

hasTypeProps(props: TypeProp | TypeProp[])

Section titled “hasTypeProps(props: TypeProp | TypeProp[])”

Returns true if all specified properties exist in a type, interface, or type literal, with optional type and optionality verification. TypeProp is an object with the form { name: string; type?: string; isOptional?: boolean }.

const explorer = await __helpers.Explorer(
'type Foo = { x: number; y: string; z?: boolean; };'
);
const { Foo } = explorer.types;
Foo.hasTypeProps({ name: 'x' }); // true
Foo.hasTypeProps([
{ name: 'x', type: 'number' },
{ name: 'y', type: 'string', isOptional: false },
{ name: 'z', isOptional: true }
]); // true
Foo.hasTypeProps({ name: 'a' }); // false

Returns true if the current node has a union type annotation whose members exactly match the specified types, ignoring order.

const explorer = await __helpers.Explorer(
'const a: number | string | boolean;'
);
const { a } = explorer.variables;
a.isUnionOf(['number', 'string', 'boolean']); // true
a.isUnionOf(['number', 'string']); // false

Checks if the current node is a type assertion (cast using as). Optionally verify the cast type matches the provided type string.

const explorer = await __helpers.Explorer(
'const a = 1 as number; const b = 2 as string;'
);
const { a, b } = explorer.variables;
a.value.hasCast(); // true
a.value.hasCast('number'); // true
a.value.hasCast('string'); // false
b.value.hasCast('string'); // true

Checks whether the current expression is a non-null assertion (!).

const explorer = await __helpers.Explorer('const a = maybeValue!;');
const { a } = explorer.variables;
a.value.hasNonNullAssertion(); // true

doesExtend(basesToCheck: string | string[])

Section titled “doesExtend(basesToCheck: string | string[])”

Checks if a class or interface extends the specified base class or interface(s). Accepts a single string or an array of strings.

const explorer = await __helpers.Explorer('class Dog extends Animal {}');
const { Dog } = explorer.classes;
Dog.doesExtend('Animal'); // true
Dog.doesExtend('Cat'); // false

doesImplement(basesToCheck: string | string[])

Section titled “doesImplement(basesToCheck: string | string[])”

Checks if a class implements the specified interface(s). Accepts a single string or an array of strings.

const explorer = await __helpers.Explorer('class Foo implements Bar, Baz { }');
const { Foo } = explorer.classes;
Foo.doesImplement('Bar'); // true
Foo.doesImplement(['Bar', 'Baz']); // true
Foo.doesImplement('Spam'); // false

typeParameters returns generic declarations (like <T>), and typeArguments returns generic usages (like <string>).

const explorer = await __helpers.Explorer(
'function id<T>(x: T): T { return x; }'
);
const { id } = explorer.functions;
id.typeParameters[0].matches('T'); // true
const explorer2 = await __helpers.Explorer(
'const m: Map<string, number> = new Map();'
);
const { m } = explorer2.variables;
m.annotation.typeArguments[0].matches('string'); // true

Use isPrivate(), isProtected(), isPublic(), and isReadOnly() on class members and type properties.

const explorer = await __helpers.Explorer(
'class Foo { private readonly x: number; }'
);
const { Foo } = explorer.classes;
const { x } = Foo.classProps;
x.isPrivate(); // true
x.isReadOnly(); // true

Use the Python AST-based helpers to inspect code structure. JavaScript utilities under __helpers.python are documented later for cases that need text processing.

These helpers parse Python source into an AST without executing it.

For Python challenges, these helpers need to be run in Python and tests should be formatted as:

({ test: () => assert(runPython(`<python code>`)) });

For complex tests, you can use assert inside Python and avoid the assert around runPython.

({ test: () => runPython(`<python code>`) });

An example would be if you need to test multiple things at once.

({
test: () =>
runPython(`
assert _Node(_code).find_class("Spam").find_function("__str__").has_return("'spam!'")
s = Spam()
assert f'{s}' == 'spam!'
`)
});

_Node is a chainable class that allows you to call methods on the result of parsing a string. To create an instance of _Node that parses the camper’s code use _Node(_code).

To access a specific statement within the code you need to chain method calls until you reach the desired scope.

For example, if the camper has written the following code:

class Spam:
def __str__(self):
return 'spam!'

To check the return value of __str__ you would write:

({
test: () =>
assert(
runPython(
`_Node(_code).find_class("Spam").find_function("__str__").has_return("'spam!'")`
)
)
});

AST-based helpers are also exposed in the Python challenges in ast_helpers module. _Node class corresponds to ast_helpers.Node:

import ast_helpers
code = '''
class Spam:
def __str__(self):
return 'spam!'
'''
print(
ast_helpers.Node(code)
.find_class("Spam")
.find_function("__str__")
.has_return("'spam!'")
)

The standalone Python examples below assume from ast_helpers import Node. In challenge tests, use the provided _Node and _code names. The helpers require parseable Python; invalid syntax raises SyntaxError.

str returns a string that would parse to the same AST as the node. For example:

function_str = """
def foo():
# will not be in the output
x = 1
"""
output_str = """def foo():
x = 1"""
str(Node(function_str)) == output_str # True

The output and source string compile to the same AST, but the output is indented with 4 spaces. Comments and trailing whitespace are removed.

Most find_ functions search the current scope rather than recursively searching nested functions or blocks. Drill into the relevant node before continuing. They return one of the following:

  • A single Node object if there can be only one match. E.g. find_function
  • A list of Node objects if there can be multiple matches. E.g.: find_ifs

When no match exists, a single-node lookup returns an empty Node, while a list lookup returns an empty list. find_function returns the first matching synchronous function; use find_async_function for an async function or find_functions for all matching definitions.

Continuing a lookup chain on a nonexistent node can raise an error. For example, Node('x = 1').find_function('missing').find_ifs() raises AttributeError.

Node('def foo():\n x = "1"').find_function("foo").has_variable("x") # True
code_str = """
class Spam(ABC):
@property
@abstractmethod
def foo(self):
return self.x
@foo.setter
@abstractmethod
def foo(self, new_x):
self.x = new_x
"""
explorer = Node(code_str)
len(explorer.find_class("Spam").find_functions("foo")) # 2
explorer.find_class("Spam").find_functions("foo")[0].is_equivalent("@property\n@abstractmethod\ndef foo(self):\n return self.x") # True
explorer.find_class("Spam").find_functions("foo")[1].is_equivalent("@foo.setter\n@abstractmethod\ndef foo(self, new_x):\n self.x = new_x") # True
Node('async def foo():\n await bar()').find_async_function("foo").is_equivalent("async def foo():\n await bar()") # True
code_str = """
async def foo(spam):
if spam:
await spam()
await bar()
await func()
"""
explorer = Node(code_str)
explorer.find_async_function("foo").find_awaits()[0].is_equivalent("await bar()") # True
explorer.find_async_function("foo").find_awaits()[1].is_equivalent("await func()") # True
explorer.find_async_function("foo").find_ifs()[0].find_awaits()[0].is_equivalent("await spam()") # True
Node("y = 2\nx = 1").find_variable("x").is_equivalent("x = 1")
Node("a: int = 1").find_variable("a").is_equivalent("a: int = 1")
Node("self.spam = spam").find_variable("self.spam").is_equivalent("self.spam = spam")

Returns a list of all variable assignments with the given name (unlike find_variable which returns only the first match).

code_str = """
x: int = 0
a.b = 0
x = 5
a.b = 2
x = 10
"""
node = Node(code_str)
len(node.find_variables("x")) # 3
node.find_variables("x")[0].is_equivalent("x: int = 0") # True
node.find_variables("x")[1].is_equivalent("x = 5") # True
node.find_variables("x")[2].is_equivalent("x = 10") # True
len(node.find_variables("a.b")) # 2
node.find_variables("a.b")[0].is_equivalent("a.b = 0") # True
node.find_variables("a.b")[1].is_equivalent("a.b = 2") # True
Node("x += 1").find_aug_variable("x").is_equivalent("x += 1")
Node("x -= 1").find_aug_variable("x").is_equivalent("x -= 1")

When the variable is out of scope, find_variable returns an None node (i.e. Node() or Node(None)):

Node('def foo():\n x = "1"').find_variable("x") == Node() # True
func_str = """
def foo():
x = 1"""
Node(func_str).find_function("foo").find_body().is_equivalent("x = 1") # True
code_str = """
def foo():
if x == 1:
return False
return True
"""
Node(code_str).find_function("foo").find_return().is_equivalent("return True") # True
Node(code_str).find_function("foo").find_ifs()[0].find_return().is_equivalent("return False") # True
code_str = """
print(1)
print(2)
foo("spam")
obj.foo("spam")
obj.bar.foo("spam")
"""
explorer = Node(code_str)
len(explorer.find_calls("print")) # 2
explorer.find_calls("print")[0].is_equivalent("print(1)")
explorer.find_calls("print")[1].is_equivalent("print(2)")
len(explorer.find_calls("foo")) # 3
explorer.find_calls("foo")[0].is_equivalent("foo('spam')")
explorer.find_calls("foo")[1].is_equivalent("obj.foo('spam')")
explorer.find_calls("foo")[2].is_equivalent("obj.bar.foo('spam')")
explorer = Node("print(1, 2)")
len(explorer.find_calls("print")[0].find_call_args()) # 2
explorer.find_calls("print")[0].find_call_args()[0].is_equivalent("1")
explorer.find_calls("print")[0].find_call_args()[1].is_equivalent("2")
class_str = """
class Foo:
def __init__(self):
pass
"""
Node(class_str).find_class("Foo").has_function("__init__") # True
if_str = """
if x == 1:
x += 1
elif x == 2:
pass
else:
return
if True:
pass
"""
Node(if_str).find_ifs()[0].is_equivalent("if x == 1:\n x += 1\nelif x == 2:\n pass\nelse:\n return")
Node(if_str).find_ifs()[1].is_equivalent("if True:\n pass")
if_str = """
if x == 1:
x += 1
elif x == 2:
pass
else:
return
if True:
pass
"""
Node(if_str).find_if("x == 1").is_equivalent("if x == 1:\n x += 1\nelif x == 2:\n pass\nelse:\n return")
Node(if_str).find_if("True").is_equivalent("if True:\n pass")
while_str = """
while True:
x += 1
else:
return
while False:
pass
"""
explorer = Node(while_str)
explorer.find_whiles()[0].is_equivalent("while True:\n x += 1\nelse:\n return") # True
explorer.find_whiles()[1].is_equivalent("while False:\n pass") # True
while_str = """
while True:
x += 1
else:
return
while False:
pass
"""
explorer = Node(while_str)
explorer.find_while("True").is_equivalent("while True:\n x += 1\nelse:\n return") # True
explorer.find_while("False").is_equivalent("while False:\n pass") # True
if_str = """
if x > 0:
x = 1
elif x < 0:
x = -1
else:
return x
"""
explorer1 = Node(if_str)
len(explorer1.find_ifs()[0].find_conditions()) # 3
explorer1.find_ifs()[0].find_conditions()[0].is_equivalent("x > 0") # True
explorer1.find_ifs()[0].find_conditions()[1].is_equivalent("x < 0") # True
explorer1.find_ifs()[0].find_conditions()[2] == Node() # True
Node("x = 1").find_conditions() # []
while_str = """
while True:
x += 1
else:
return
while False:
pass
"""
explorer2 = Node(while_str)
explorer2.find_whiles()[0].find_conditions()[0].is_equivalent("True") # True
explorer2.find_whiles()[0].find_conditions()[1] == Node() # True
explorer2.find_whiles()[1].find_conditions()[0].is_equivalent("False") # True
for_str = """
dict = {'a': 1, 'b': 2, 'c': 3}
for x, y in enumerate(dict):
print(x, y)
else:
pass
for i in range(4):
pass
"""
explorer = Node(for_str)
explorer.find_for_loops()[0].is_equivalent("for x, y in enumerate(dict):\n print(x, y)\nelse:\n pass") # True
explorer.find_for_loops()[1].is_equivalent("for i in range(4):\n pass") # True
for_str = """
dict = {'a': 1, 'b': 2, 'c': 3}
for x, y in enumerate(dict):
print(x, y)
else:
pass
for i in range(4):
pass
"""
explorer = Node(for_str)
explorer.find_for("(x, y)", "enumerate(dict)").is_equivalent("for x, y in enumerate(dict):\n print(x, y)\nelse:\n pass") # True
explorer.find_for("i", "range(4)").is_equivalent("for i in range(4):\n pass") # True
for_str = """
dict = {'a': 1, 'b': 2, 'c': 3}
for x, y in enumerate(dict):
print(x, y)
else:
pass
for i in range(4):
pass
"""
explorer = Node(for_str)
explorer.find_for_loops()[0].find_for_vars().is_equivalent("(x, y)") # True
explorer.find_for_loops()[1].find_for_vars().is_equivalent("i") # True
for_str = """
dict = {'a': 1, 'b': 2, 'c': 3}
for x, y in enumerate(dict):
print(x, y)
else:
pass
for i in range(4):
pass
"""
explorer = Node(for_str)
explorer.find_for_loops()[0].find_for_iter().is_equivalent("enumerate(dict)") # True
explorer.find_for_loops()[1].find_for_iter().is_equivalent("range(4)") # True
if_str = """
if True:
x = 1
elif False:
x = 2
"""
explorer1 = Node(if_str)
explorer1.find_ifs()[0].find_bodies()[0].is_equivalent("x = 1") # True
explorer1.find_ifs()[0].find_bodies()[1].is_equivalent("x = 2") # True
while_str = """
while True:
x += 1
else:
x = 0
while False:
pass
"""
explorer2 = Node(while_str)
explorer2.find_whiles()[0].find_bodies()[0].is_equivalent("x += 1") # True
explorer2.find_whiles()[0].find_bodies()[1].is_equivalent("x = 0") # True
explorer2.find_whiles()[1].find_bodies()[0].is_equivalent("pass") # True
for_str = """
dict = {'a': 1, 'b': 2, 'c': 3}
for x, y in enumerate(dict):
print(x, y)
else:
print(x)
for i in range(4):
pass
"""
explorer3 = Node(for_str)
explorer3.find_for_loops()[0].find_bodies()[0].is_equivalent("print(x, y)") # True
explorer3.find_for_loops()[0].find_bodies()[1].is_equivalent("print(x)") # True
explorer3.find_for_loops()[1].find_bodies()[0].is_equivalent("pass") # True
code_str = """
import ast, sys
from math import factorial as f
"""
explorer = Node(code_str)
len(explorer.find_imports()) # 2
explorer.find_imports()[0].is_equivalent("import ast, sys")
explorer.find_imports()[1].is_equivalent("from math import factorial as f")

Returns a list of list comprehensions, set comprehensions, dictionary comprehensions and generator expressions nodes not assigned to a variable or part of other statements.

code_str = """
[i**2 for i in lst]
(i for i in lst)
{i * j for i in spam for j in lst}
{k: v for k,v in dict}
comp = [i for i in lst]
"""
explorer = Node(code_str)
len(explorer.find_comps()) # 4
explorer.find_comps()[0].is_equivalent("[i**2 for i in lst]")
explorer.find_comps()[1].is_equivalent("(i for i in lst)")
explorer.find_comps()[2].is_equivalent("{i * j for i in spam for j in lst}")
explorer.find_comps()[3].is_equivalent("{k: v for k,v in dict}")

Returns a list of comprehension/generator expression iterables. It can be chained to find_variable, find_return, find_call_args()[n].

code_str = """
x = [i**2 for i in lst]
def foo(spam):
return [i * j for i in spam for j in lst]
"""
explorer = Node(code_str)
len(explorer.find_variable("x").find_comp_iters()) # 1
explorer.find_variable("x").find_comp_iters()[0].is_equivalent("lst")
len(explorer.find_function("foo").find_return().find_comp_iters()) # 2
explorer.find_function("foo").find_return().find_comp_iters()[0].is_equivalent("spam")
explorer.find_function("foo").find_return().find_comp_iters()[1].is_equivalent("lst")

Returns a list of comprehension/generator expression targets (i.e. the iteration variables).

code_str = """
x = [i**2 for i in lst]
def foo(spam):
return [i * j for i in spam for j in lst]
"""
explorer = Node(code_str)
len(explorer.find_variable("x").find_comp_targets()) # 1
explorer.find_variable("x").find_comp_targets()[0].is_equivalent("i")
len(explorer.find_function("foo").find_return().find_comp_targets()) # 2
explorer.find_function("foo").find_return().find_comp_targets()[0].is_equivalent("i")
explorer.find_function("foo").find_return().find_comp_targets()[1].is_equivalent("j")

Returns the dictionary comprehension key.

code_str = """
x = {k: v for k,v in dict}
def foo(spam):
return {k: v for k in spam for v in lst}
"""
explorer = Node(code_str)
explorer.find_variable("x").find_comp_key().is_equivalent("k")
explorer.find_function("foo").find_return().find_comp_key().is_equivalent("k")

Returns the expression evaluated at each iteration of comprehensions/generator expressions. It includes the if/else portion. In case only the if is present, use find_comp_ifs.

code_str = """
x = [i**2 if i else -1 for i in lst]
def foo(spam):
return [i * j for i in spam for j in lst]
"""
explorer = Node(code_str)
explorer.find_variable("x").find_comp_expr().is_equivalent("i**2 if i else -1")
explorer.find_function("foo").find_return().find_comp_expr().is_equivalent("i * j")

Returns a list of comprehension/generator expression if conditions. The if/else construct instead is part of the expression and is found with find_comp_expr.

code_str = """
x = [i**2 if i else -1 for i in lst]
def foo(spam):
return [i * j for i in spam if i > 0 for j in lst if j != 6]
"""
explorer = Node(code_str)
len(explorer.find_variable("x").find_comp_ifs()) # 0
len(explorer.find_function("foo").find_return().find_comp_ifs()) # 2
explorer.find_function("foo").find_return().find_comp_ifs()[0].is_equivalent("i > 0")
explorer.find_function("foo").find_return().find_comp_ifs()[1].is_equivalent("j != 6")

Returns a list of all try statements.

code_str = """
try:
x = 1 / 0
except ZeroDivisionError:
print("division by zero")
else:
print("no error")
finally:
print("cleanup")
try:
y = int("abc")
except:
pass
"""
node = Node(code_str)
len(node.find_trys()) # 2
node.find_trys()[0].is_equivalent("try:\n x = 1 / 0\nexcept ZeroDivisionError:\n print('division by zero')\nelse:\n print('no error')\nfinally:\n print('cleanup')") # True
node.find_trys()[1].is_equivalent("try:\n y = int('abc')\nexcept:\n pass") # True

Returns a list of all except handlers in a try statement.

code_str = """
try:
x = 1 / 0
except ZeroDivisionError:
print("division by zero")
except ValueError as e:
print(f"value error: {e}")
except:
print("other error")
"""
node = Node(code_str)
try_stmt = node.find_trys()[0]
len(try_stmt.find_excepts()) # 3

is_equivalent() cannot be used on the items found by find_excepts() because a standalone except raises a SyntaxError.

Returns a specific except handler matching the exception type and optional variable name.

code_str = """
try:
x = 1 / 0
except ZeroDivisionError:
print("division by zero")
except ValueError as e:
print(f"value error: {e}")
except:
print("other error")
"""
node = Node(code_str)
try_stmt = node.find_trys()[0]
try_stmt.find_except("ZeroDivisionError").find_body().is_equivalent("print('division by zero')") # True
try_stmt.find_except("ValueError", "e").find_body().is_equivalent("print(f'value error: {e}')") # True
try_stmt.find_except().find_body().is_equivalent("print('other error')") # True (bare except)

Returns True if a try statement has an except handler for the given exception type and variable name.

code_str = """
try:
x = 1 / 0
except ZeroDivisionError:
print("division by zero")
except ValueError as e:
print(f"value error: {e}")
"""
node = Node(code_str)
try_stmt = node.find_trys()[0]
try_stmt.has_except("ZeroDivisionError") # True
try_stmt.has_except("ValueError", "e") # True
try_stmt.has_except("ValueError") # False

Returns the else block of a try statement.

code_str = """
try:
x = 1
except ValueError:
print("error")
else:
print("success")
x = 2
"""
node = Node(code_str)
try_stmt = node.find_trys()[0]
try_stmt.find_try_else().is_equivalent("print('success')\nx = 2") # True

Returns the finally block of a try statement.

code_str = """
try:
x = 1
except ValueError:
print("error")
finally:
print("cleanup")
x = None
"""
node = Node(code_str)
try_stmt = node.find_trys()[0]
try_stmt.find_finally().is_equivalent("print('cleanup')\nx = None") # True

Returns a list of match statements.

code_str = """
match x:
case 0:
pass
case _:
pass
match y:
case 1:
pass
"""
node = Node(code_str)
len(node.find_matches()) # 2
node.find_matches()[0].is_equivalent("match x:\n case 0:\n pass\n case _:\n pass") # True

Returns the subject of a match statement.

code_str = """
match x:
case 0:
pass
"""
node = Node(code_str)
node.find_matches()[0].find_match_subject().is_equivalent("x") # True

Returns a list of case blocks in a match statement.

code_str = """
match x:
case 0:
print(0)
print('spam')
case _:
pass
"""
node = Node(code_str)
len(node.find_matches()[0].find_match_cases()) # 2
node.find_matches()[0].find_match_cases()[0].find_body().is_equivalent("print(0)\nprint('spam')") # True
node.find_matches()[0].find_match_cases()[1].find_body().is_equivalent("pass") # True

is_equivalent() cannot be used on the items found by find_match_cases() because a standalone case raises a SyntaxError.

Returns the pattern of a case block.

code_str = """
match x:
case 0:
pass
case [a, b]:
pass
case _:
pass
"""
node = Node(code_str)
node.find_matches()[0].find_match_cases()[0].find_case_pattern().is_equivalent("0") # True
node.find_matches()[0].find_match_cases()[1].find_case_pattern().is_equivalent("[a, b]") # True
node.find_matches()[0].find_match_cases()[2].find_case_pattern().is_equivalent("_") # True

Returns the guard condition of a case block.

code_str = """
match x:
case 0 if y > 0:
pass
case [a, b] if y == -1:
pass
case _:
pass
"""
node = Node(code_str)
node.find_matches()[0].find_match_cases()[0].find_case_guard().is_equivalent("y > 0") # True
node.find_matches()[0].find_match_cases()[1].find_case_guard().is_equivalent("y == -1") # True
node.find_matches()[0].find_match_cases()[2].find_case_guard().is_empty() # True (no guard)

get_ functions return the value of the node, not the node itself.

Returns the value stored in a variable’s ast.Constant initializer, such as a number, string, boolean, or None. It returns None if the variable is not found.

Node("x = 1").get_variable("x") # 1

The helper reads the initializer’s .value attribute; it does not evaluate the assignment. Expressions and collections such as x = 1 + 1, x = [1, 2], and x = {"a": 1} raise AttributeError because their AST nodes do not have that attribute. Even x = -1 raises AttributeError: Python represents the minus sign as a unary operation around the constant 1.

has_ functions return a boolean indicating whether the node exists.

Node("x = 1").has_variable("x") # True
Node("def foo():\n pass").has_function("foo") # True

Returns a boolean indicating if the specified statement is found.

Node("name = input('hi')\nself.matrix[1][5] = 3").has_stmt("self.matrix[1][5] = 3") # True
Node("def foo(*, a, b, c=0):\n pass").find_function("foo").has_args("*, a, b, c=0") # True
Node("def foo():\n pass").find_function("foo").has_pass() # True
Node("if x==1:\n x+=1\nelse: pass").find_ifs()[0].find_bodies()[1].has_pass() # True

Returns a boolean indicating if the function returns the specified expression/object.

code_str = """
def foo():
if x == 1:
return False
return True
"""
explorer = Node(code_str)
explorer.find_function("foo").has_return("True") # True
explorer.find_function("foo").find_ifs()[0].has_return("False") # True

Returns a boolean indicating if the function has the specified return annotation.

Node("def foo() -> int:\n return 0").find_function("foo").has_returns("int") # True
Node("def foo() -> 'spam':\n pass").find_function("foo").has_returns("'spam'") # True
code_str = """
class A:
@property
@staticmethod
def foo():
pass
"""
Node(code_str).find_class("A").find_function("foo").has_decorators("property") # True
Node(code_str).find_class("A").find_function("foo").has_decorators("property", "staticmethod") # True
Node(code_str).find_class("A").find_function("foo").has_decorators("staticmethod", "property") # False, order does matter

Checks for a matching standalone expression in the current scope. The arguments must match too. It does not recursively search nested statements or find calls inside assignments and return expressions.

code_str = """
print(math.sqrt(25))
if True:
spam()
"""
explorer = Node(code_str)
explorer.has_call("print(math.sqrt(25))")
explorer.find_ifs()[0].find_bodies()[0].has_call("spam()")

Searches for a loaded name or attribute within the entire AST, or within the first top-level function selected by function. It can find names used in nested expressions, but it does not check that they are the target of a call. For example, Node('saved = print').block_has_call('print') returns True even though print is not called. Aliases are not resolved.

code_str = """
srt = sorted([5, 1, 9])
def foo(lst):
return sorted(lst)
def spam(lst):
return lst.sort()
def eggs(dictionary):
if True:
k = dictionary.get(key)
"""
node = Node(code_str)
node.block_has_call("sorted", "foo") # True
node.block_has_call("sorted") # True
node.block_has_call("sort", "spam") # True
node.block_has_call("get", "eggs") # True
node.block_has_call("get") # True
node.block_has_call("split") # False

Sometimes a challenge requires learners to remove a specific statement, such as a print() call.

Negating has_call only excludes the exact standalone expression in the current scope. Negating block_has_call also rejects references that are not calls. These checks are not interchangeable:

Node("print(123)").has_call("print()") # False
Node("print(123)").block_has_call("print") # True
Node("saved = print").block_has_call("print") # True
code_str = """
import ast, sys
from math import factorial as f
"""
explorer = Node(code_str)
explorer.has_import("import ast, sys")
explorer.has_import("from math import factorial as f")
code_str = """
class spam:
pass
"""
Node(code_str).has_class("spam")

Compares normalized source produced by parsing and unparsing the node and the target string. This ignores comments and formatting differences, but does not establish that two programs behave the same. For example, x = 1 + 1 does not match x = 2. The target must be parseable on its own, so standalone except and case clauses cannot be used as target strings.

Node("x = 1").is_equivalent("x = 1") # True
Node("\nx = 1").is_equivalent("x = 1") # True
Node("x = 1").is_equivalent("x = 2") # False

This is syntactic sugar for == Node().

Node().is_empty() # True
Node("x = 1").find_variable("x").is_empty() # False

This allows you to check if the return value of a function call is assigned to a variable.

explorer = Node("def foo():\n x = bar()")
explorer.find_function("foo").find_variable("x").value_is_call("bar") # True
Node("x = 1").find_variable("x").is_integer() # True
Node("x = '1'").find_variable("x").is_integer() # False
Node("class C(A, B):\n pass").find_class("C").inherits_from("A") # True
Node("class C(A, B):\n pass").find_class("C").inherits_from("A", "B") # True

Returns a boolean indicating if the statements passed as arguments are found in the same order in the tree (statements can be non-consecutive).

code_str = """
x = 1
if x:
print("x is:")
y = 0
print(x)
return y
x = 0
"""
if_str = """
if x:
print("x is:")
y = 0
print(x)
return y
"""
explorer = Node(code_str)
explorer.is_ordered("x=1", "x=0") # True
explorer.is_ordered("x=1", if_str, "x=0") # True
explorer.find_ifs()[0].is_ordered("print('x is:')", "print(x)", "return y") # True
explorer.is_ordered("x=0", "x=1") # False
explorer.find_ifs()[0].is_ordered("print(x)", "print('x is:')") # False
stmts = """
if True:
pass
x = 1
"""
Node(stmts)[1].is_equivalent("x = 1") # True

Prefer the Python AST-based helpers for code structure. The following JavaScript utilities process Python source as text.

Use __helpers.python.removeComments(code) to remove Python comments from code strings:

const codeWithComments = `
def test(): # This is a comment
return True # Another comment
`;
const cleaned = __helpers.python.removeComments(codeWithComments);
// Returns: "\ndef test(): \n return True \n"

This regex-based utility is not a Python tokenizer. It also removes JavaScript-style // and /* */ text, so it can corrupt valid Python: value = 10 // 2 becomes value = 10 , and a # inside a string starts removal too. Prefer AST helpers for structural checks; they ignore comments while preserving strings and operators.

Available via __helpers.python.getDef(code, functionName), this function extracts function definition details from Python code:

const pythonCode = `
def calculate_area(length, width):
"""Calculate rectangle area"""
return length * width
`;
// Via __helpers.python.getDef
const funcInfo = __helpers.python.getDef(pythonCode, 'calculate_area');
// Returns: {
// def: "def calculate_area(length, width):\n \"\"\"Calculate rectangle area\"\"\"\n return length * width\n",
// function_parameters: "length, width",
// function_body: " \"\"\"Calculate rectangle area\"\"\"\n return length * width\n",
// function_indentation: 0
// }

This returns the first regex match, or null when no match exists. The regex does not parse Python function syntax: return annotations and parentheses inside default values can prevent a match. Whitespace, including the trailing newline in this example, is retained. Prefer find_function, find_async_function, and find_body when inspecting a definition.

Available via __helpers.python.getBlock(code, blockPattern), this function extracts an indented block by matching text immediately before its colon against a string or regex. Strings are escaped and matched literally; regex inputs contribute their source pattern. Include the keyword and condition or loop expression to match the intended header. For example:

const pythonCode = `
if x > 0:
print("positive")
else:
print("not positive")
for i in range(5):
print(i)
`;
// Extract if block
const ifBlock = __helpers.python.getBlock(pythonCode, 'if x > 0');
// {
// block_body: ' print("positive")',
// block_condition: 'if x > 0',
// block_indentation: 0
// }
// Extract for block
const forBlock = __helpers.python.getBlock(pythonCode, 'for i in range(5)');
// {
// block_body: ' print(i)\n',
// block_condition: 'for i in range(5)',
// block_indentation: 0
// }

Passing only 'if' or 'for' returns null for this example because the condition or loop expression is missing from the pattern. The helper returns only the matched block body, not adjacent elif or else branches. It relies on spaces for indentation and does not parse Python structure. Regex flags from blockPattern are discarded; matching uses the helper’s s and m flags. Prefer find_if, find_for, find_while, and their body helpers for structural checks.

  • Python does not allow newline characters between keywords and their arguments. E.g:
def
add(x, y):
return x + y
  • Python does allow newline characters between function arguments. E.g:
def add(
x,
y
):
return x + y