Adds the conformance/no-undeclared-analytics-event rule at warn severity,
mirroring no-undeclared-audit and no-undeclared-event-publish. The rule
cross-checks analytics.track("X", ...) literal slugs in *.use-case.ts
files against manifest.useCases[name].analyticsEvents, providing
sub-second editor feedback before boot-time conformance fires.
- Extends _manifest-ast.js to parse analyticsEvents arrays in both
extractUseCaseEntry helpers
- Registers rule in plugin.js and base.js at ["warn", { repoRoot }]
- RuleTester fixtures: declared pass, undeclared warn, non-literal no-op,
non-use-case file no-op, missing manifest entry no-op
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
64 lines
2.1 KiB
JavaScript
64 lines
2.1 KiB
JavaScript
import { parseManifestUseCases } from "./_manifest-ast.js";
|
|
import { useCaseNameFromFile } from "./_usecase-name.js";
|
|
import {
|
|
manifestPathForFeature,
|
|
featureRootForFile,
|
|
} from "./_manifest-source.js";
|
|
|
|
/** @type {import("eslint").Rule.RuleModule} */
|
|
export default {
|
|
meta: {
|
|
type: "problem",
|
|
docs: {
|
|
description:
|
|
'analytics.track("X") inside a use-case factory must declare X in manifest.useCases[name].analyticsEvents.',
|
|
},
|
|
schema: [
|
|
{
|
|
type: "object",
|
|
properties: { repoRoot: { type: "string" } },
|
|
additionalProperties: false,
|
|
},
|
|
],
|
|
messages: {
|
|
undeclared:
|
|
'{{useCase}} calls analytics.track("{{event}}") but {{event}} is not declared in manifest.useCases.{{useCase}}.analyticsEvents. Add it to the manifest or remove the call.',
|
|
},
|
|
},
|
|
create(context) {
|
|
const opts = context.options[0] ?? {};
|
|
const repoRoot = opts.repoRoot ?? context.cwd ?? process.cwd();
|
|
const filename = context.filename;
|
|
const useCaseName = useCaseNameFromFile(filename);
|
|
if (!useCaseName) return {};
|
|
const featureRoot = featureRootForFile(filename, repoRoot);
|
|
if (!featureRoot) return {};
|
|
const manifest = parseManifestUseCases(manifestPathForFeature(featureRoot));
|
|
if (!manifest || !manifest[useCaseName]) return {};
|
|
const declared = new Set(manifest[useCaseName].analyticsEvents ?? []);
|
|
return {
|
|
CallExpression(node) {
|
|
if (
|
|
node.callee.type === "MemberExpression" &&
|
|
node.callee.object.type === "Identifier" &&
|
|
node.callee.object.name === "analytics" &&
|
|
node.callee.property.type === "Identifier" &&
|
|
node.callee.property.name === "track" &&
|
|
node.arguments.length > 0 &&
|
|
node.arguments[0].type === "Literal" &&
|
|
typeof node.arguments[0].value === "string"
|
|
) {
|
|
const event = node.arguments[0].value;
|
|
if (!declared.has(event)) {
|
|
context.report({
|
|
node,
|
|
messageId: "undeclared",
|
|
data: { event, useCase: useCaseName },
|
|
});
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|