vscode/.eslint-plugin-local/code-no-localized-model-description.ts
Rob Lourens 676ae78fa5
Fix localized tool markdownDescriptions (#277589)
* Fix localized tool markdownDescriptions
And add a lint rule

* Just keep this the same

* Fixes
2025-11-15 13:12:37 -08:00

129 lines
3.7 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TSESTree } from '@typescript-eslint/utils';
import * as eslint from 'eslint';
import * as visitorKeys from 'eslint-visitor-keys';
import type * as ESTree from 'estree';
const MESSAGE_ID = 'noLocalizedModelDescription';
type NodeWithChildren = TSESTree.Node & {
[key: string]: TSESTree.Node | TSESTree.Node[] | null | undefined;
};
type PropertyKeyNode = TSESTree.Property['key'] | TSESTree.MemberExpression['property'];
type AssignmentTarget = TSESTree.AssignmentExpression['left'];
export default new class NoLocalizedModelDescriptionRule implements eslint.Rule.RuleModule {
meta: eslint.Rule.RuleMetaData = {
messages: {
[MESSAGE_ID]: 'modelDescription values describe behavior to the language model and must not use localized strings.'
},
type: 'problem',
schema: false
};
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
const reportIfLocalized = (expression: TSESTree.Expression | null | undefined) => {
if (expression && containsLocalizedCall(expression)) {
context.report({ node: expression, messageId: MESSAGE_ID });
}
};
return {
Property: (node: ESTree.Property) => {
const propertyNode = node as TSESTree.Property;
if (!isModelDescriptionKey(propertyNode.key, propertyNode.computed)) {
return;
}
reportIfLocalized(propertyNode.value as TSESTree.Expression);
},
AssignmentExpression: (node: ESTree.AssignmentExpression) => {
const assignment = node as TSESTree.AssignmentExpression;
if (!isModelDescriptionAssignmentTarget(assignment.left)) {
return;
}
reportIfLocalized(assignment.right);
}
};
}
};
function isModelDescriptionKey(key: PropertyKeyNode, computed: boolean | undefined): boolean {
if (!computed && key.type === 'Identifier') {
return key.name === 'modelDescription';
}
if (key.type === 'Literal' && key.value === 'modelDescription') {
return true;
}
return false;
}
function isModelDescriptionAssignmentTarget(target: AssignmentTarget): target is TSESTree.MemberExpression {
if (target.type === 'MemberExpression') {
return isModelDescriptionKey(target.property, target.computed);
}
return false;
}
function containsLocalizedCall(expression: TSESTree.Expression): boolean {
let found = false;
const visit = (node: TSESTree.Node) => {
if (found) {
return;
}
if (isLocalizeCall(node)) {
found = true;
return;
}
for (const key of visitorKeys.KEYS[node.type] ?? []) {
const value = (node as NodeWithChildren)[key];
if (Array.isArray(value)) {
for (const child of value) {
if (child) {
visit(child);
if (found) {
return;
}
}
}
} else if (value) {
visit(value);
}
}
};
visit(expression);
return found;
}
function isLocalizeCall(node: TSESTree.Node): boolean {
if (node.type === 'CallExpression') {
return isLocalizeCallee(node.callee);
}
if (node.type === 'ChainExpression') {
return isLocalizeCall(node.expression);
}
return false;
}
function isLocalizeCallee(callee: TSESTree.CallExpression['callee']): boolean {
if (callee.type === 'Identifier') {
return callee.name === 'localize';
}
if (callee.type === 'MemberExpression') {
if (!callee.computed && callee.property.type === 'Identifier') {
return callee.property.name === 'localize';
}
if (callee.property.type === 'Literal' && callee.property.value === 'localize') {
return true;
}
}
return false;
}