Skip to content

Body order

bodyOrder enforces the order of statements inside a component or function body. Along with usage rules, it is one of the two places Shapelint looks past the top-level declaration surface.

A pattern deliberately treats the body as a lower bound and never inspects it (see Pattern matching). bodyOrder is the opt-in exception: it fixes the relative order of the statement groups you name, and ignores everything else.

ts
{
  name: 'component-body-order',
  files: 'components/**/*.tsx',
  bodyOrder: {
    groups: [
      { name: 'custom hooks', match: ['use*'] },
      { name: 'useRef',       match: ['useRef'] },
      { name: 'useState',     match: ['useState', 'useReducer'] },
      { name: 'functions',    match: ['function'] },
      { name: 'useEffect',    match: ['useEffect', 'useLayoutEffect'] },
      { name: 'return',       match: ['return'] },
    ],
  },
}

This makes the following component conform, and flags any statement that appears out of order:

tsx
export const Widget = () => {
  const { user } = useAuth(); // custom hooks
  const ref = useRef(null); // useRef
  const [count, setCount] = useState(0); // useState
  function handleClick() {
    setCount((c) => c + 1);
  } // functions
  useEffect(() => log(count), [count]); // useEffect
  return (
    <div ref={ref} onClick={handleClick}>
      {user}
      {count}
    </div>
  ); // return
};

The BodyOrderSpec shape

FieldDescription
groupsThe required order of statement groups, top to bottom. Required.
inName of the declaration to inspect. Omit to check the exported function(s).
interGroupSpacingSpacing between different statement groups: 'always' (requires blank line), 'never' (forbids blank line), 'optional' (default).
intraGroupSpacingSpacing between statements within the same group: 'never' (keeps clustered, forbids blank line), 'always' (requires blank line between statements), 'optional' (default).

Which body is checked

  • With in, the top-level function/arrow declaration of that name.
  • Without in, every exported function/arrow declaration, falling back to every top-level function/arrow when nothing is exported. Non-exported helpers are left alone, so only the component itself is ordered.

Only the direct statements of the body are ordered; nested blocks (an if, a render helper defined inside the component) are not.

How a statement joins a group

Each group lists match selectors. A statement joins a group when it matches any selector:

SelectorMatches
a call namea statement that is, or is initialized by, a call to that callee, const r = useRef(), useEffect(...), const { x } = useThing(). * is a wildcard: use*, use*Query.
'function'a function value: see below.
'variable' / 'const'a plain variable declaration (not a function, not initialized by a hook/call), e.g. `const sheetBg = bg
'return'the return statement.

A literal call name outranks a wildcard. useRef() matches both useRef and use*, so with the config above it lands in the useRef group, not the custom hooks catch-all above it. This is what lets "custom hooks first, then the specific built-ins" work. To pull a specific hook into a group explicitly, name it literally there (e.g. add useCallback to functions).

The 'function' selector

'function' matches a statement that declares a function value, in any of these three forms:

tsx
function handleClick() {} // declaration
const handleClick = () => {}; // arrow const
const handleReset = function () {}; // function-expression const

The one exception is a const initialized by a call, such as const handleClick = useCallback(...). That is a call first, so it classifies by its callee (useCallback / use*), not as 'function'.

The 'variable' / 'const' selector

'variable' (and its alias 'const') matches plain variable declarations that:

  1. Are not function declarations or arrow/function expression initializers (which match 'function').
  2. Are not initialized by a call expression or hook (e.g. useRef(...), useAuth()).

This lets you explicitly place derived state, local variables, and computed expressions in the component lifecycle:

tsx
const sheetBg = bg || colors.card; // plain variable / derived state
const isPending = isA || isB;       // plain variable / derived state
ts
{ name: 'derived state', match: ['variable'] } // or match: ['const']

Spacing controls (interGroupSpacing & intraGroupSpacing)

bodyOrder can enforce vertical padding between statement groups to keep code clean and readable:

ts
bodyOrder: {
  interGroupSpacing: 'always', // requires at least one blank line between distinct groups
  intraGroupSpacing: 'never',  // keeps statements in the same group clustered together
  groups: [
    { name: 'custom hooks', match: ['use*'] },
    { name: 'useRef', match: ['useRef'] },
    { name: 'useState', match: ['useState'] },
    { name: 'derived state', match: ['variable'] },
    { name: 'return', match: ['return'] },
  ],
}

interGroupSpacing

  • 'always': Requires at least one blank line separating distinct statement groups. Emits ARCH_BODY_INTER_SPACING if statements from different groups sit on consecutive lines.
  • 'never': Forbids blank lines between different statement groups.
  • 'optional' (default): Ignores spacing between groups.

intraGroupSpacing

  • 'never': Forbids blank lines between statements within the same group (keeps them clustered together). Emits ARCH_BODY_INTRA_SPACING if blank lines appear between statements in the same group.
  • 'always': Requires a blank line between each statement in the same group.
  • 'optional' (default): Ignores spacing within groups.

Example

tsx
export const Widget = () => {
  const auth = useAuth();
  const { theme } = useTheme();

  const ref1 = useRef(null);
  const ref2 = useRef(null);

  const [count, setCount] = useState(0);

  const isPositive = count > 0;

  return <div ref={ref1}>{isPositive && count}</div>;
};
  • Distinct groups (custom hooks, useRef, useState, derived state, return) are separated by blank lines.
  • Statements in the same group (e.g. ref1 and ref2) are clustered together without blank lines.

Autofix (shapelint fix)

Running shapelint fix --write automatically resolves body order and spacing violations:

  • Reorders statements to match the configured groups sequence.
  • Joins statements in the same group with single newlines (\n) (or double newlines if intraGroupSpacing: 'always').
  • Separates distinct groups with double newlines (\n\n) (or single newlines if interGroupSpacing: 'never').
  • Preserves indentation, block structure, and comments.

What is not constrained

A statement that matches no group is ignored: it can appear anywhere without causing a violation. The rule only asserts that the groups you named keep their relative order:

tsx
export const Widget = () => {
  const ref = useRef(null);
  console.log('debug'); // unclassified, fine anywhere
  const [n, setN] = useState(0);
  return (
    <div ref={ref}>
      {n}
    </div>
  );
};

Backend & service method ordering

bodyOrder is equally effective for backend services, controllers, and pipeline functions. Use in to target specific methods (such as execute or handle):

ts
{
  name: 'service-method-order',
  files: 'services/**/*.service.ts',
  bodyOrder: {
    in: 'execute',
    groups: [
      { name: 'validation', match: ['validate*'] },
      { name: 'logging',    match: ['this.logger.*'] },
      { name: 'return',     match: ['return'] },
    ],
  },
}

This enforces that method inputs are validated before log statements and execution logic, flagging any late validation statement as an ARCH_BODY_ORDER violation.

Combining with a pattern rule

bodyOrder can live on the same rule as a pattern, or on its own rule whose files glob overlaps. All rules whose glob matches a file apply to it in array order (see rule ordering), so a shape rule and an order rule stack:

ts
rules: [
  { name: 'ui-component', files: 'components/ui/**/*.tsx', pattern: `…` },
  { name: 'component-body-order', files: 'components/**/*.tsx', bodyOrder },
];

Here components/ui/Button.tsx is checked by both rules. Note the short-circuit: if ui-component's pattern fails, that error is reported first and bodyOrder is skipped for that file until the shape is fixed. To have the order enforced even when the shape rule would otherwise claim the file first, this stacking is what you want, a single pattern-only rule never inspects the body. To keep everything in one place instead, add the bodyOrder key directly to the ui-component rule.

Diagnostic codes

  • ARCH_BODY_ORDER, a statement appears after a group that should come later. The message names both groups and echoes the expected order.
  • ARCH_BODY_INTRA_SPACING, statements within the same group violate intraGroupSpacing (e.g. unwanted blank lines).
  • ARCH_BODY_INTER_SPACING, statements between different groups violate interGroupSpacing (e.g. missing blank line separator).

Released under the MIT License.