Skip to content

Member contracts

A pattern governs a file's top-level declaration surface. Member contracts reach inside a class, its methods and their decorators, the constructor's injected parameters, and correlated groups of declarations, so an architecture's structural conventions are enforced, not just its file shapes and names.

All of these are separate rule options; mix them freely with pattern on the same files.

methods

For every class member the where selector matches, methods asserts a per-member contract. It takes one spec or an array of specs.

ts
{
  name: 'controller',
  files: '**/*.controller.ts',
  methods: {
    where: '@(Get|Post|Put|Delete|Patch)',   // selector
    require: ['@ApiResponse'],                // all must be present
    oneOf: ['@Protected', '@Public'],         // exactly one must be present
  },
}

Selectors (where)

where has two dialects:

  • Decorator selector: @Name, an alternation @(Get|Post|Put), or a glob @Api*. Selects members carrying a matching decorator.
  • Signature selector: modifier words followed by a name glob: 'public *', 'private get*', 'static handle'. Recognized modifiers: public, private, protected, static, readonly, async, get, set. (A member with no explicit visibility counts as public.)

Omit where to select every method.

Decorator requirements

  • require: string[], every listed decorator must co-occur. A route handler missing @ApiResponse is ARCH_MEMBER_PATTERN.
  • oneOf: string[], exactly one of the group must be present. This is how you force every route to be explicitly guarded xor public, neither (an accidentally-unguarded handler) nor both is allowed.
  • anyOf: string[], at least one of the group must be present.

oneOf / anyOf failures are ARCH_MEMBER_DECORATOR_GROUP.

Decorator presence is structural, and squarely Shapelint's job. Two things it deliberately does not check: a member's return/parameter types (that is tsc's job) and whether a particular function is called inside a body (a behavioral assertion, use ESLint or the semantic tier).

Constructor / DI

The inject option (named inject, not constructor, to avoid colliding with Object.prototype.constructor) enforces a dependency-injection contract on each top-level class.

ts
{
  name: 'service',
  files: '**/*.service.ts',
  inject: {
    require: ['private readonly $_: Logger'],          // params that must be present
    allowParams: ['*Service', '*Repository', 'Logger'], // allow-list of injected types
  },
}
  • require, parameter templates. Modifiers named in the template (private readonly) must be present on the actual parameter; $_ matches any name/type; the type may be a hole template.
  • allowParams, restricts every injected parameter type to a glob allow-list.

Both are structural: require asserts a constructor parameter exists and allowParams bounds the injected types. A class missing the injection, an injected type outside the allow-list, or (when require is set) a constructor entirely, fails ARCH_CONSTRUCTOR_CONTRACT.

optionalNames / pairWith

A pattern is a fixed lower bound: every listed declaration must exist. Files that export a varying set of correlated declarations need a softer tool.

optionalNames lists templates for exports that may appear, but each that does must follow the correlated naming:

ts
{
  name: 'dto',
  files: '**/*.dto.ts',
  pattern: `export type \${Name} = $_\nexport const \${Name}Schema = $_`,
  optionalNames: ['${NAME}_QUERY', '${NAME}_BODY', '${NAME}_RESPONSE'],
}

A file with USERS_BODY passes; one where the same member is misnamed in camelCase (usersBody), or correlated to the wrong file word (ACCOUNTS_BODY in users.dto.ts), fails ARCH_MEMBER_CORRELATION. Absent optional members are fine.

pairWith is a pairing rule: for every declaration matching when, a declaration matching require (with the correlated hole) must also exist.

ts
pairWith: { when: 'const ${x}Schema', require: 'class ${X}' }

A userSchema with no User class, or a class for the wrong word, fails ARCH_MEMBER_CORRELATION.

memberOrder

A house style often orders class members by group, fields, then the constructor, then the public API, then private helpers. methods can pin a member's shape but not the relative order of member groups. memberOrder declares that order as a list of member GROUPS:

ts
{
  name: 'class-member-order',
  files: '**/*.service.ts',
  memberOrder: ['field', 'constructor', 'public-method', 'private-method'],
}

Each entry is a member kind: field, constructor, method, getter, setter, optionally prefixed with a visibility/modifier: public, protected, private, static, or decorated (public-method, static-field, decorated-method). A bare kind (field, method) matches any visibility.

Like body order, it is a lower bound:

  • a member is classified into the first listed group it matches;
  • members matching no listed group are ignored, they neither create nor break ordering;
  • only the relative order between listed groups is enforced, no gap, count, or blank-line rules.
ts
// ✗ ARCH_MEMBER_ORDER, a private method precedes the public API
export class UserService {
  private helper() {} // private-method …
  getUser() {} // … before public-method
}

// ✓ conforms
export class UserService {
  private db: Db; // field
  constructor() {} // constructor
  getUser() {} // public-method
  private helper() {} // private-method
}

A getter in the second class above would be ignored (getters aren't listed), so it can sit anywhere without tripping the rule. The diagnostic points at the out-of-order member and reports ARCH_MEMBER_ORDER. memberOrder stacks with pattern and methods on the same rule.

decoratorArgs

Decorator arguments are otherwise opaque. decoratorArgs validates a positional string argument against a naming convention and/or correlates it to the filename.

ts
{
  name: 'controller',
  files: '**/*.controller.ts',
  decoratorArgs: [
    { name: 'Route', arg: 0, convention: 'kebab-case', correlateToFilename: true },
  ],
}

@Route('UserStuff') on users.controller.ts fails (not kebab and not correlated); @Route('users') passes. Failures are ARCH_DECORATOR_ARG.

Released under the MIT License.