Skip to content

Extending Mule-Lint

Mule-Lint can be extended through its library API. Custom rules must be registered with a LintEngine instance; configuration entries alone do not load executable modules.

Create a Per-File Rule

Extend BaseRule when a rule can decide from one parsed Mule XML document:

import { ALL_RULES, BaseRule, Issue, LintEngine, ValidationContext } from '@sfdxy/mule-lint';

export class RequiredCategoryRule extends BaseRule {
  readonly id = 'ACME-001';
  readonly name = 'Required logger category';
  readonly description = 'Requires every logger to declare a category';
  readonly severity = 'warning' as const;
  readonly category = 'logging' as const;

  validate(doc: Document, context: ValidationContext): Issue[] {
    return this.select('//mule:logger[not(@category)]', doc).map((logger) =>
      this.createIssue(logger, 'Logger is missing a category', {
        filePath: context.filePath,
        suggestion: 'Add a stable application-specific category.',
      }),
    );
  }
}

const engine = new LintEngine({
  rules: [...ALL_RULES, new RequiredCategoryRule()],
});

const report = await engine.scan('./my-mule-project');

this.select() uses the namespace-aware XPath helper. this.createIssue() supplies the rule metadata and source location. Rule-specific options are available with this.getOption(context, key, fallback).

Create a Project Rule

Extend ProjectRule when validation needs several files, non-XML assets, or project structure:

import { Issue, ProjectRule, ValidationContext } from '@sfdxy/mule-lint';

export class OrganizationLayoutRule extends ProjectRule {
  readonly id = 'ACME-002';
  readonly name = 'Organization layout';
  readonly description = 'Checks organization-specific project layout';
  readonly severity = 'warning' as const;
  readonly category = 'structure' as const;

  protected validateProject(context: ValidationContext): Issue[] {
    if (context.files.some((file) => file.relativePath === 'pom.xml')) return [];

    return [
      this.createProjectIssue('Project is missing pom.xml', {
        suggestion: 'Add the Maven project descriptor at the repository root.',
      }),
    ];
  }
}

The engine invokes project validation once per scan. A hybrid rule may extend BaseRule, implement normal validate() logic, and also provide runProject(context) for a cross-file phase.

Configure a Registered Rule

Once registered, a rule can be disabled, have its severity changed, or receive options:

const engine = new LintEngine({
  rules: [...ALL_RULES, new RequiredCategoryRule()],
  config: {
    rules: {
      'ACME-001': {
        enabled: true,
        severity: 'error',
        options: { requiredPrefix: 'com.acme' },
      },
    },
  },
});

The CLI configuration supports extends, rules, include, exclude, defaultFormatter, failOnWarning, and qualityGate. extends selects one built-in rule profile; explicit per-rule configuration still wins. The CLI does not dynamically import custom rule files. The reserved keys maxIssues currently warns and has no runtime effect.

A TypeScript rule must be compiled and passed to the engine, as above. For a check that is a single XPath expression, customRulesPath avoids writing code at all — see custom XPath rules. It never imports executable code: a custom rule is an expression and a message template.

Test the Rule

Tests should cover both valid and invalid input and assert the rule ID, message, severity, and useful location data. For a per-file rule, parse an XML string and call validate(). For project behavior, prefer an engine scan over a fixture directory so the lifecycle and shared context are exercised.

Before publishing or contributing an extension, run:

npm run check

For built-in rules, also export the class, add exactly one instance to ALL_RULES, document it in the rules catalog, and add focused regression tests.