Files
react-native-devtools-frontend/scripts/localization/localizationV2Checks.js
T
Christy Chen 138c3a87b6 [Loc] Presubmit#2: prevent using of LocV1 APIs in migrated files
The check will prevent the using of V1 APIs in the files under the directory that we determine as 'migrated', or when UIStrings is declared in a file.
Two way to test
1. add to any .js file that calls ls``
export const UIStrings = {
  /**
  *@description a fake string
  */
  fakeString: 'a fakeString',
};

or

in check_localized_strings.js
Update `const migratedDirsSet = new Set([]);`
To `const migratedDirsSet = new Set(['settings', 'console']);`

2. run check_localizable_resources.js

The check will complain about some some files have APIs need to be migrated

See Loc design doc #Presubmit section for details
https://docs.google.com/document/d/1L6TkT2-42MMQ72ZSBMFwUaq7M6mDgA2X0x8oHHKaV_U/edit#heading=h.w1no7qaa0mi0


Bug: 941561
Change-Id: I4f8fba41ac1dd393fdb399d7e580e9a9ba39143a
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2244052
Commit-Queue: Christy Chen <chrche@microsoft.com>
Reviewed-by: Tim van der Lippe <tvanderlippe@chromium.org>
Reviewed-by: Vidal Diazleal <vidorteg@microsoft.com>
Reviewed-by: Simon Zünd <szuend@chromium.org>
Reviewed-by: Peter Marshall <petermarshall@chromium.org>
2020-09-09 15:55:23 +00:00

179 lines
6.3 KiB
JavaScript

// Copyright 2020 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.
/**
* This script is part of the presubmit check that parses DevTools frontend files,
* collects localizable strings, and run some checks for localization.
*
* If argument '--autofix' is present, try fixing the error automatically
*/
const fs = require('fs');
const ts = require('typescript');
const writeFileAsync = fs.promises.writeFile;
const parseLocalizableResources = require('./utils/check_localized_strings');
/**
* Verifies that all strings in UIStrings structure are called with localization API.
*/
async function checkUIStrings(shouldAutoFix) {
const localizationCallsMap = parseLocalizableResources.localizationCallsMap;
const uiStringsMap = parseLocalizableResources.uiStringsMap;
const errorMap = new Map();
for (const [filePath, uiStringsEntries] of uiStringsMap.entries()) {
let errorList;
if (filePath.endsWith('moduleUIStrings.js')) {
const newFilePath = filePath.replace('moduleUIStrings.js', 'module.json');
const stringIdSet = getStringIdsFromCallSites(localizationCallsMap.get(newFilePath));
errorList = checkStringEntries(uiStringsEntries, stringIdSet, true);
} else {
const stringIdSet = getStringIdsFromCallSites(localizationCallsMap.get(filePath));
errorList = checkStringEntries(uiStringsEntries, stringIdSet, false);
}
if (errorList.length > 0) {
errorMap.set(filePath, errorList);
}
}
if (errorMap.size > 0) {
if (shouldAutoFix) {
return autoFixUIStringsCheck(errorMap);
}
return addUIStringsCheckError(errorMap);
}
return;
}
/**
* Get all the string entries called with localization API from the entry map of that file.
* Returns a set of the string IDs.
*/
function getStringIdsFromCallSites(entryFromCallsMap) {
const stringIdSet = new Set();
if (entryFromCallsMap) {
for (const entry of entryFromCallsMap) {
stringIdSet.add(entry.stringId);
}
}
return stringIdSet;
}
/**
* Check if any unused string is in UIStrings structure.
*/
function checkStringEntries(uiStringsEntries, stringIdSet, isModuleJSON) {
const unusedEntries = [];
for (const stringEntry of uiStringsEntries) {
if (isModuleJSON) {
if (!stringIdSet.has(stringEntry.stringValue)) {
unusedEntries.push(stringEntry);
}
} else {
if (!stringIdSet.has(stringEntry.stringId)) {
unusedEntries.push(stringEntry);
}
}
}
return unusedEntries;
}
/**
* Add UIStrings check error message to the Loc V2 check error.
*/
function addUIStringsCheckError(errorMap) {
let UIStringsCheckErrorMessage = 'Unused string found in UIStrings.\n' +
'Please remove them from UIStrings, or add the localization calls in your code.\n\n';
for (const [filePath, uiStringsEntries] of errorMap.entries()) {
UIStringsCheckErrorMessage += `${filePath}\n`;
for (const entry of uiStringsEntries) {
UIStringsCheckErrorMessage += ` "${entry.stringValue}"\n`;
}
}
return UIStringsCheckErrorMessage;
}
/**
* Auto-fixing UIString check error by removing unused strings in UIStrings structure.
*/
async function autoFixUIStringsCheck(errorMap) {
let autoFixUIStringsMessage = '\nUnused string found in UIStrings.';
const promises = [];
for (const [filePath, unusedUIStringsEntries] of errorMap.entries()) {
let content = fs.readFileSync(filePath, 'utf8');
content = removeUnusedEntries(filePath, content, unusedUIStringsEntries);
promises.push(writeFileAsync(filePath, content));
autoFixUIStringsMessage += `\nReplaced UIStrings in ${filePath}`;
}
await Promise.all(promises);
return autoFixUIStringsMessage;
}
/**
* Remove unused entries from UIStrings and return the new file content
*/
function removeUnusedEntries(filePath, content, unusedUIStringsEntries) {
const textToRemoveList = getTextToRemove(filePath, content, unusedUIStringsEntries);
for (const text of textToRemoveList) {
// check if the trailing comma present (the last entry may or may not have it)
if (content[content.indexOf(text) + text.length] === ',') {
content = content.replace(`${text},`, '');
} else {
content = content.replace(text, '');
}
}
return content;
}
/**
* Find the full text of unused entries in UIStrings
*/
function getTextToRemove(filePath, content, unusedUIStringsEntries) {
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.ESNext, true);
const unusedStringIds = new Set(unusedUIStringsEntries.map(entry => entry.stringId));
const collectUnusedPropertyTextsFromNode = node => {
// check through the properties to see if the name matches a stringId that should be removed
const unusedPropertyTexts = [];
for (const property of node.initializer.properties) {
if (unusedStringIds.has(property.name.escapedText)) {
// get the full text of the entry including descriptions and placeholders
unusedPropertyTexts.push(property.getFullText());
}
}
return unusedPropertyTexts;
};
const uiStringsNode = parseLocalizableResources.findUIStringsNode(sourceFile);
return collectUnusedPropertyTextsFromNode(uiStringsNode);
}
/**
* Verifies that there are no V1 APIs added in a directories that are migrated.
* The check will be removed when the migration process is done.
*/
function checkNoV1CallsInMigratedDir() {
const filesContainV1Calls = parseLocalizableResources.locV1CallsInMigratedFiles;
if (filesContainV1Calls.size === 0) {
return;
}
fileMigratedError = 'Localization V1 APIs used in these files that have already migrated to V2:\n';
for (const filePath of filesContainV1Calls) {
fileMigratedError += `\n${filePath}`;
}
fileMigratedError += '\nAutofix are not supported for this check. Please manually update V1 APIs to V2 APIs.';
fileMigratedError += `\nFor example:
ls("An example string") ---> i18n.i18n.getLocalizedString(_str, UIStrings.theExampleString)
and then add it to UIStrings:
const UIStrings = { theExampleString: 'An example string' } with descriptions.`;
fileMigratedError += '\nFor more details. See devtools-frontend\\src\\docs\\localization\\README.md';
return fileMigratedError;
}
module.exports = {
checkUIStrings,
removeUnusedEntries,
checkNoV1CallsInMigratedDir,
};