blob: d7066402bc2b9c62e67fea904eb7b0b456ec4faf [file] [log] [blame] [edit]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type {TSESTree} from '@typescript-eslint/utils';
import {createRule} from './utils/ruleCreator.ts';
export default createRule({
name: 'enforce-version-controller-methods',
meta: {
type: 'problem',
docs: {
description:
'Enforces that VersionController has the correct number of update methods and that they are contiguous.',
category: 'Possible Errors',
},
messages: {
incorrectMethodCount:
'VersionController.CURRENT_VERSION is {{currentVersion}} but there are {{methodCount}} updateVersion methods. These numbers should match.',
nonContiguousMethods:
'Missing or non-contiguous updateVersion method: expected updateVersionFrom{{expectedFrom}}To{{expectedTo}}',
},
schema: [],
},
defaultOptions: [],
create: function(context) {
return {
ClassDeclaration(node) {
if (node.id?.name !== 'VersionController') {
return;
}
let currentVersion: number|undefined;
let currentVersionNode: TSESTree.Node|undefined;
const updateMethods: Array<{from: number, to: number, node: TSESTree.Node}> = [];
for (const element of node.body.body) {
if (element.type === 'PropertyDefinition' && element.static && element.key.type === 'Identifier' &&
element.key.name === 'CURRENT_VERSION') {
if (element.value?.type === 'Literal' && typeof element.value.value === 'number') {
currentVersion = element.value.value;
currentVersionNode = element.key;
}
}
if (element.type === 'MethodDefinition' && element.key.type === 'Identifier') {
const match = element.key.name.match(/^updateVersionFrom(\d+)To(\d+)$/);
if (match) {
updateMethods.push({
from: parseInt(match[1], 10),
to: parseInt(match[2], 10),
node: element.key,
});
}
}
}
if (currentVersion === undefined) {
// If we can't find CURRENT_VERSION, assume it's valid or being enforced by another rule/type system.
return;
}
if (updateMethods.length !== currentVersion) {
context.report({
node: currentVersionNode || node,
messageId: 'incorrectMethodCount',
data: {
currentVersion,
methodCount: updateMethods.length,
}
});
// Don't report non-contiguous methods if the count is wrong to avoid spam
return;
}
// Sort the methods just in case they are out of order in the file
updateMethods.sort((a, b) => a.from - b.from);
for (let i = 0; i < currentVersion; ++i) {
const expectedFrom = i;
const expectedTo = i + 1;
const actual = updateMethods[i];
if (actual?.from !== expectedFrom || actual.to !== expectedTo) {
context.report({
node: actual ? actual.node : (currentVersionNode || node),
messageId: 'nonContiguousMethods',
data: {
expectedFrom,
expectedTo,
}
});
break; // Only report the first missing method
}
}
},
};
},
});