Pattern Modifiers

Various modifiers can be applied to patterns to transform how they are matched. All pattern modifiers yield a new pattern with the modified behavior. Importantly, none of these modifiers represent base patterns by themselves, but instead modify or combine other patterns.

and clause

and clauses are used to combine one or more comma-separated patterns.

The and clause matches only if all of the patterns match.

In the example below, we combine the where clause with and to rewrite two types of React hooks in one pattern.

PATTERN
arrow_function($body) where $body <: and {
  contains js"React.useState" => js"useState",
  contains js"React.useMemo" => js"useMemo",
}
INPUTOUTPUT
const someComponent = () => {
    const [count, setCount] = React.useState(0);
}

const anotherComponent = () => {
  const [count, setCount] = React.useState(0);
  const math = React.useMemo(() => count + 5, [count]);
}
const someComponent = () => {
    const [count, setCount] = React.useState(0);
}

const anotherComponent = () => {
  const [count, setCount] = useState(0);
  const math = useMemo(() => count + 5, [count]);
}

or clause

or clauses are used to match against multiple patterns. They are written using the or keyword, and their arguments are separated by a comma.

The or clause matches if any of the patterns match.

or is short-circuited, so the first pattern that matches will be the one that is used.

PATTERN
arrow_function($body) where $body <: or {
  contains js"React.useState" => js"useState",
  contains js"React.useMemo" => js"useMemo",
}
INPUTOUTPUT
const someComponent = () => {
  const [count, setCount] = React.useState(0);
}

const anotherComponent = () => {
  const [count, setCount] = React.useState(0);
  const math = React.useMemo(() => count + 5, [count]);
}
const someComponent = () => {
  const [count, setCount] = useState(0);
}

const anotherComponent = () => {
  const [count, setCount] = useState(0);
  const math = React.useMemo(() => count + 5, [count]);
}

any clause

The any clause is a non-short-circuiting version of or. It will match if any of the provided patterns matches, but will continue to try to match all of the patterns and execute any applicable transformations:

PATTERN
arrow_function($body) where $body <: any {
  contains js"React.useState" => js"useState",
  contains js"React.useMemo" => js"useMemo",
}
INPUTOUTPUT
const someComponent = () => {
  const [count, setCount] = React.useState(0);
}

const anotherComponent = () => {
  const [count, setCount] = React.useState(0);
  const math = React.useMemo(() => count + 5, [count]);
}
const someComponent = () => {
  const [count, setCount] = useState(0);
}

const anotherComponent = () => {
  const [count, setCount] = useState(0);
  const math = useMemo(() => count + 5, [count]);
}

not clause

not clauses are used to negate a pattern. They are written using the not keyword.

A not clause matches if the pattern it negates does not match.

PATTERN
`$method($message)` => `console.warn($message)` where {
  $method <: not `console.error`
}
INPUTOUTPUT
console.log('Hello, world!');
console.error('Hello, world!');
console.warn('Hello, world!');
console.error('Hello, world!');

maybe clause

maybe clauses are used to optionally match against a pattern. If the pattern inside does not match, the maybe clause will still match as a whole.

PATTERN
`throw new Error($err)` as $thrown => `throw new CustomError($err);` where {
  $err <: maybe string(fragment=$fun) => `{ message: $err }`
}
INPUTOUTPUT
if (Math.random() < 0.5) {
  throw new Error();
} else {
  throw new Error('nest the message in an object');
}
if (Math.random() < 0.5) {
  throw new CustomError();
} else {
  throw new CustomError({ message: 'nest the message in an object' });
}

Warning: In the pattern above, the metavariable $fun serves only an illustrative example and so isn't actually used after it's assigned. However, if we were to use it, we'd have to be careful: metavariables in maybe clauses are bound only if the maybe clause matches, and Grit will throw an error if you attempt to use one when the clause does not match.

contains operator

The contains keyword is used to modify a pattern to match any node that contains a specific pattern by traversing downwards through the syntax tree.

GRIT
`function ($_) { $body }` where {
  $body <: contains `console.log`
}

In the example above, the pattern will only functions where the $body metavariable contains a console.log statement.

until modifier

The until modifier is appended to contains pattern is used to stop traversal within a contains clause.

The contains pattern will only traverse within the parts of the node that are not matched by the until pattern. When the until pattern is reached, traversal will not continue any deeper.

For example, the until here stops traversal inside sanitized calls:

GRIT
`console.$_($content)` where {
  $content <: contains `secret` until `sanitized($_)`
}
JAVASCRIPT
console.log(secret);
console.log(sanitized(secret));
console.log(random(secret));
console.log(other + sanitized(secret));
console.log(random(secret) + sanitized(secret));

as modifier

as can be used to assign a matched snippet to a metavariable. This is often useful when you want to perform some change on a section of code as a whole while also making more granular changes within that section. For example:

PATTERN
engine marzano(0.1)
language js

`function $name ($args) { $body }` as $func where {
  $func => `const $name = ($args) => { $body }`,
  $args <: contains `apple` => `mango`
}
INPUTOUTPUT
function fruits (apple, pear) {
  console.log("fruits");
}
const fruits = (mango, pear) => { console.log("fruits"); }

within clause

within restricts the pattern to only match if the target node appears within code matching another pattern.

For example, the pattern below only matches the second console.log, as it appears within the if block.

GRIT
`console.log($arg)` where {
  $arg <: within `if (DEBUG) { $_ }`
}
TYPESCRIPT
console.log('Hello, world!');
if (DEBUG) {
  console.log('Hello, world!');
}

after clause

after clauses are used to restrict the pattern to only match if the target node is directly after a node matching a specific pattern. They are written using the after keyword.

GRIT
`console.warn($_)` as $warn where {
  $warn <: after `console.log($_)`
}

some clause

The some clause can be used to match a metavariable which represents a list against a pattern which represents some element of that list. The list can be any ordered set of syntax tree nodes, for example, a list of statements.

As long as one of the nodes matches the pattern, the some clause will match.

PATTERN
`var $x = [$names]` => `var coolPeople = [$names]` where {
  $names <: some { `"andrew"` }
}
INPUTOUTPUT
var people = ['alex', 'andrew', 'susan'];
var peopleTwo = ['beth', 'max', 'right'];
var coolPeople = ['alex', 'andrew', 'susan'];
var peopleTwo = ['beth', 'max', 'right'];

Tip: If you would like the some clause to succeed even if no match is found, prefix it with maybe. Ex. $names <: maybe some { `"andrew"` }

List patterns

List patterns are used to match against a list of terms. They are written using the [] syntax.

The list pattern matches if all of the terms in the list match in order.

PATTERN
`var $x = [$numbers]` => `var firstPrimes = [$numbers]` where {
  $numbers <: [ `2`, `3`, `5` ]
}
INPUTOUTPUT
var someNumbers = [2, 3, 5];
var firstPrimes = [2, 3, 5];

... clause

The ... clause is used to match against a sublist of a list, ignoring the ... elements.

PATTERN
`var $x = [$numbers]` => `var firstPrimes = [$numbers]` where {
  $numbers <: [ `2`, `3`, ..., `11` ]
}
INPUTOUTPUT
var someNumbers = [2, 3, 5, 7, 11];
var firstPrimes = [2, 3, 5, 7, 11];

Assignment =

The assignment operator = is used to assign a value to a metavariable, much in the same way as you would assign to a variable in other programming languages. The value can be anything that can appear on the right-hand side of a rewrite.

GRIT
`console.log($message)` as $log where {
  $new_log_call = `logger.log($message)`,
  $log => $new_log_call
}