-
Notifications
You must be signed in to change notification settings - Fork 498
/
Copy pathl10n-no-unused-message.ts
159 lines (139 loc) · 5.31 KB
/
l10n-no-unused-message.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type {TSESLint, TSESTree} from '@typescript-eslint/utils';
import {isUIStringsVariableDeclarator} from './utils/l10n-helper.ts';
import {createRule} from './utils/ruleCreator.ts';
type MemberExpression = TSESTree.MemberExpression;
type Property = TSESTree.Property;
type Identifier = TSESTree.Identifier;
type RuleFix = TSESLint.RuleFix;
type RuleFixer = TSESLint.RuleFixer;
const MODULE_UI_STRINGS_FILENAME_REGEX = /ModuleUIStrings\.(js|ts)$/;
const TRACE_INSIGHTS_UI_STRINGS_FILENAME_REGEX = /models\/trace\/insights\/.*\.(js|ts)$/;
/**
* Returns true iff the passed expression is of the form `UIStrings.bar`.
*/
function isStandardUIStringsMemberExpression(
expr: TSESTree.Node,
): expr is MemberExpression&{
object: Identifier, property: Identifier,
}
{
if (expr.type !== 'MemberExpression') {
return false;
}
if (expr.object.type !== 'Identifier' || !expr.object.name.startsWith('UIStrings')) {
return false;
}
return expr.property.type === 'Identifier';
}
export default createRule({
name: 'l10n-no-unused-message',
meta: {
type: 'problem',
docs: {
description: 'Unused properties on UIStrings objects are not allowed.',
category: 'Possible Errors',
},
fixable: 'code',
schema: [], // no options
messages: {
unusedMessage: 'UIStrings message is not used.',
},
},
defaultOptions: [],
create: function(context) {
// ESLint provides the filename with normalized separators.
const filename = context.filename.replaceAll(
'\\',
'/',
);
const sourceCode = context.sourceCode;
// Store the Property node itself to report errors and apply fixes
const declaredUIStringsKeys = new Map<string, Property>();
const usedUIStringsKeys = new Set<string>();
function removeProperty(fixer: RuleFixer, property: Property): RuleFix {
const source = sourceCode;
// For simplicity, we remove whole lines. This assumes that the UIStrings has
// some standard formatting. Otherwise we would have to fiddle a lot
// with tokens and whitespace.
let lineToRemoveStart = source.getLocFromIndex(property.range[0]).line;
const lineToRemoveEnd = source.getLocFromIndex(property.range[1]).line + 1;
// Are there comments in front of the property?
// Move the line we want to remove to the line of the first comment.
const comments = source.getCommentsBefore(property);
const firstComment = comments[0];
if (firstComment?.range) {
lineToRemoveStart = source.getLocFromIndex(firstComment.range[0]).line;
}
const removeStart = source.getIndexFromLoc({
line: lineToRemoveStart,
column: 0,
});
// Ensure removeEnd doesn't exceed the source length
const lastLineIndex = source.lines.length - 1;
const lastLine = source.lines[lastLineIndex];
const maxIndex = source.getIndexFromLoc({
line: lastLineIndex + 1,
column: lastLine.length,
});
const calculatedRemoveEnd = source.getIndexFromLoc({
line: Math.min(lineToRemoveEnd, source.lines.length),
column: 0,
});
// Adjust removeEnd if it points beyond the end of the file
const removeEnd = Math.min(calculatedRemoveEnd, maxIndex);
// Ensure start is not greater than end (can happen with empty files or edge cases)
if (removeStart >= removeEnd) {
// Cannot create a valid fix, maybe return null or an empty array?
// For now, returning a no-op fix to match original intent closely.
// A better approach might be to log a warning or prevent the report.
console.warn(
`Could not generate fix for unused l10n message at ${filename}:${lineToRemoveStart}`,
);
return fixer.replaceTextRange([0, 0], ''); // No-op fix
}
return fixer.removeRange([removeStart, removeEnd]);
}
return {
VariableDeclarator(node) {
if (MODULE_UI_STRINGS_FILENAME_REGEX.test(filename) ||
TRACE_INSIGHTS_UI_STRINGS_FILENAME_REGEX.test(filename)) {
return;
}
if (!isUIStringsVariableDeclarator(context, node)) {
return;
}
// Add checks for init and expression properties
if (node.init?.type !== 'TSAsExpression' || node.init.expression.type !== 'ObjectExpression') {
return;
}
for (const property of node.init.expression.properties) {
if (property.type !== 'Property' || property.key.type !== 'Identifier') {
continue;
}
declaredUIStringsKeys.set(property.key.name, property);
}
},
MemberExpression(node) {
if (!isStandardUIStringsMemberExpression(node)) {
return;
}
usedUIStringsKeys.add(node.property.name);
},
'Program:exit': function() {
for (const usedKey of usedUIStringsKeys) {
declaredUIStringsKeys.delete(usedKey);
}
for (const property of declaredUIStringsKeys.values()) {
context.report({
node: property,
messageId: 'unusedMessage',
fix: fixer => removeProperty(fixer, property),
});
}
},
};
},
});