Skip to content

@fedify/lint: actor-id-required/actor-inbox-property-required/actor-shared-inbox-property-required false positive when dispatcher returns null #974

Description

@Jae-Hyuk-Jang

Environment

  • Package/tool: @fedify/lint (oxlint plugin)
  • Runtime: Node.js
  • Operating system: Linux (WSL2)

Steps to reproduce

Register an actor dispatcher that returns null for "not found" (as documented in ActorDispatcher's JSDoc) before returning a Person with id, inbox, and endpoints.sharedInbox all set:

federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
  const user = /* lookup */;
  if (user == null) return null;

  return new Person({
    id: ctx.getActorUri(identifier),
    inbox: ctx.getInboxUri(identifier),
    endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }),
  });
});

Run oxlint on the file.

Expected behavior

No warning/error. The Person-returning branch already sets id, inbox, and endpoints.sharedInbox as recommended, and returning null for "actor not found" is explicitly documented as valid:

  • ActorDispatcher's return type includes | null (packages/fedify/src/federation/callback.ts).
  • setActorDispatcher's JSDoc says the dispatcher "may return an actor, a Tombstone, or null if the actor is not found" (packages/fedify/src/federation/federation.ts).
  • The handler treats a null return as a normal 404, not an error (packages/fedify/src/federation/handler.ts).

Actual behavior

actor-id-required (error), actor-inbox-property-required, and actor-shared-inbox-property-required all fire on the dispatcher, even
though the Person-returning path is fully compliant.

Cause

In packages/lint/src/lib/property-checker.ts, checkAllReturnPaths requires every return statement in the dispatcher body to satisfy the property checker:

const checkAllReturnPaths = (propertyChecker: PropertyChecker) =>
(node) =>
  pipe(
    node,
    collectReturnPaths,
    cases(isEmpty, always(false), every(checkReturnStatement(propertyChecker))),
  );

checkReturnStatement treats a non-object argument (such as null) as an automatic failure via always(false):

const checkReturnStatement =
  (propertyChecker: PropertyChecker) => (node: ReturnStatement) =>
    pipe(
      node,
      prop("argument"),
      cases(isObject, checkBranchWith(propertyChecker), always(false)),
    );

Since every() requires all collected return paths to pass, a documented return null; early-exit branch makes the whole function fail the check, regardless of whether the actual actor-returning branch is compliant.

Proposed fix

Exclude early-exit return null; (and possibly return new Tombstone(...), which also isn't subject to these property requirements) from the paths checkAllReturnPaths requires to carry the checked property -- only returns of an actor object should be checked.

Notes

Found while adding a DB-backed actor dispatcher for a small ActivityPub project. Verified by removing the null branch: oxlint reports 0
warnings/errors with just return new Person({...}), and reports the 3 false positives above as soon as the if (user == null) return null; branch is added back.

AI usage disclosure

This issue draft was assisted-by Claude Code:claude-sonnet-5, which helped read through @fedify/lint's source and trace the root cause. The reproduction, cited source locations, and final write-up were reviewed and verified by me before submitting.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Effort

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions