| // 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 |
| } |
| } |
| }, |
| }; |
| }, |
| }); |