Files
react-native-devtools-frontend/scripts/localization/check_localizable_resources.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

335 lines
12 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 .js and
* module.json files, collects localizable strings, checks if frontend strings are
* in .grd/.grdp files and reports error if present.
*
* If argument '--autofix' is present, add the new resources to and remove unused
* messages from GRDP files.
*/
const fs = require('fs');
const writeFileAsync = fs.promises.writeFile;
const appendFileAsync = fs.promises.appendFile;
const checkLocalizedStrings = require('./utils/check_localized_strings');
const localizationUtils = require('./utils/localization_utils');
const localizationV2Checks = require('./localizationV2Checks');
const grdpFileStart = '<?xml version="1.0" encoding="utf-8"?>\n<grit-part>\n';
const grdpFileEnd = '</grit-part>';
async function main() {
try {
const shouldAutoFix = process.argv.includes('--autofix');
const error = await checkLocalizedStrings.validateGrdAndGrdpFiles(shouldAutoFix);
if (error !== '' && !shouldAutoFix) {
throw new Error(error);
}
await checkLocalizedStrings.parseLocalizableResourceMaps();
if (shouldAutoFix) {
await autofix(error);
} else {
await getErrors();
}
} catch (e) {
console.log(e.stack);
process.exit(1);
}
}
main();
async function getErrors() {
let error = getV1Errors();
error += await getV2Errors();
if (error === '') {
console.log('DevTools localizable resources checker passed.');
return;
}
error += '\nSome errors are potentially fixable with the `--autofix` option.';
throw new Error(error);
}
function getV1Errors() {
const toAddError = checkLocalizedStrings.getAndReportResourcesToAdd();
const toModifyError = checkLocalizedStrings.getAndReportIDSKeysToModify();
const toRemoveError = checkLocalizedStrings.getAndReportResourcesToRemove();
const localizabilityError = checkLocalizedStrings.getLocalizabilityError();
const v1ErrorMessage = `${toAddError || ''}${toModifyError || ''}${toRemoveError || ''}${localizabilityError || ''}`;
return v1ErrorMessage;
}
async function getV2Errors() {
const checkUIStringsError = await localizationV2Checks.checkUIStrings();
const checkMigratedDirectoryError = localizationV2Checks.checkNoV1CallsInMigratedDir();
const v2ErrorMessage = `${checkUIStringsError || ''}${checkMigratedDirectoryError || ''}`;
return v2ErrorMessage;
}
async function autofix(existingError) {
let autoFixMessage = await autofixV1(existingError);
autoFixMessage += await autofixV2();
if (autoFixMessage === '') {
console.log('DevTools localizable resources checker passed.');
return;
}
autoFixMessage += '\n';
autoFixMessage += '\nUse git status to see what has changed.';
throw new Error(autoFixMessage);
}
async function autofixV1(existingError) {
const localizabilityError = checkLocalizedStrings.getLocalizabilityError();
const keysToAddToGRD = checkLocalizedStrings.getMessagesToAdd();
const keysToRemoveFromGRD = checkLocalizedStrings.getMessagesToRemove();
const resourceAdded = await addResourcesToGRDP(keysToAddToGRD, keysToRemoveFromGRD);
const resourceModified = await modifyResourcesInGRDP();
const resourceRemoved = await removeResourcesFromGRDP(keysToRemoveFromGRD);
const shouldAddExampleTag = checkShouldAddExampleTag(keysToAddToGRD);
const isV1checksPassed =
!localizabilityError && !resourceAdded && !resourceRemoved && !resourceModified && existingError === '';
let message = '';
if (!isV1checksPassed) {
message +=
'Found changes to localizable DevTools resources.\nDevTools localizable resources checker has updated the appropriate grd/grdp file(s).';
if (existingError !== '') {
message +=
`\nGrd/Grdp files have been updated. Please verify the updated grdp files and/or the <part> file references in ${
localizationUtils.getRelativeFilePathFromSrc(localizationUtils.GRD_PATH)} are correct.`;
}
if (resourceAdded) {
message += '\nManually write a description for any new <message> entries.';
if (shouldAddExampleTag) {
message += ' Add example tag(s) <ex> for messages that contain placeholder(s)';
}
message += '\nFor more details, see src/docs/localization/grdp_files.md';
}
if (resourceRemoved && duplicateRemoved(keysToRemoveFromGRD)) {
message += '\nDuplicate <message> entries are removed. Please verify the retained descriptions are correct.';
}
if (localizabilityError) {
message += localizabilityError;
}
}
return message;
}
async function autofixV2() {
const checkAndFixUIStringsError = await localizationV2Checks.checkUIStrings(true);
const checkMigratedDirectoryError = localizationV2Checks.checkNoV1CallsInMigratedDir();
let message = '';
if (checkAndFixUIStringsError) {
message += `\n${checkAndFixUIStringsError}`;
}
if (checkMigratedDirectoryError) {
message += `\n${checkMigratedDirectoryError}`;
}
return message;
}
function checkShouldAddExampleTag(keys) {
if (keys.size === 0) {
return false;
}
const stringObjs = [...keys.values()];
const stringsWithArgument = stringObjs.filter(stringObj => !!stringObj.arguments);
return stringsWithArgument.length > 0;
}
function duplicateRemoved(keysToRemoveFromGRD) {
for (const [, messages] of keysToRemoveFromGRD) {
if (messages.length > 1) {
return true;
}
}
return false;
}
// Return true if any resources are added
async function addResourcesToGRDP(keysToAddToGRD, keysToRemoveFromGRD) {
function mapGRDPFilePathToStrings(keysToAddToGRD, keysToRemoveFromGRD) {
const grdpFilePathToStrings = new Map();
// Get the grdp files that need to be modified
for (const [key, stringObj] of keysToAddToGRD) {
if (!grdpFilePathToStrings.has(stringObj.grdpPath)) {
grdpFilePathToStrings.set(stringObj.grdpPath, []);
}
// Add the IDS key to stringObj so we have access to it later
stringObj.ids = key;
// If the same key is to be removed, this is likely a string copy to another folder.
// Keep the string from grd to preserve all <ex> and <ph> tags and the description.
if (keysToRemoveFromGRD.has(key)) {
const existingMessage = keysToRemoveFromGRD.get(key);
stringObj.grdString = existingMessage[0].grdString;
stringObj.description = checkLocalizedStrings.getLongestDescription(existingMessage);
}
grdpFilePathToStrings.get(stringObj.grdpPath).push(stringObj);
}
return grdpFilePathToStrings;
}
if (keysToAddToGRD.size === 0) {
return false;
}
// Map grdp file path to strings to be added to that file so that we only need to
// modify every grdp file once
const grdpFilePathToStrings = mapGRDPFilePathToStrings(keysToAddToGRD, keysToRemoveFromGRD);
const promises = [];
const grdpFilePathsToAdd = [];
for (const [grdpFilePath, initialStringsToAdd] of grdpFilePathToStrings) {
let stringsToAdd = initialStringsToAdd;
// The grdp file doesn't exist, so create one.
if (!fs.existsSync(grdpFilePath)) {
let grdpMessagesToAdd = '';
for (const stringObj of stringsToAdd) {
grdpMessagesToAdd += localizationUtils.createGrdpMessage(stringObj.ids, stringObj);
}
// Create a new grdp file and reference it in the parent grd file
promises.push(appendFileAsync(grdpFilePath, `${grdpFileStart}${grdpMessagesToAdd}${grdpFileEnd}`));
grdpFilePathsToAdd.push(grdpFilePath);
continue;
}
const grdpFileContent = await localizationUtils.parseFileContent(grdpFilePath);
const grdpFileLines = grdpFileContent.split('\n');
let newGrdpFileContent = '';
const IDSRegex = new RegExp(`"(${localizationUtils.IDSPrefix}.*?)"`);
for (let i = 0; i < grdpFileLines.length; i++) {
const grdpLine = grdpFileLines[i];
const match = grdpLine.match(IDSRegex);
// match[0]: full match
// match[1]: message IDS key
if (match) {
const ids = match[1];
const stringsToAddRemaining = [];
for (const stringObj of stringsToAdd) {
// Insert the new <message> in sorted order.
if (ids > stringObj.ids) {
newGrdpFileContent += localizationUtils.createGrdpMessage(stringObj.ids, stringObj);
} else {
stringsToAddRemaining.push(stringObj);
}
}
stringsToAdd = stringsToAddRemaining;
} else if (grdpLine.includes(grdpFileEnd)) {
// Just hit the end tag, so insert any remaining <message>s.
for (const stringObj of stringsToAdd) {
newGrdpFileContent += localizationUtils.createGrdpMessage(stringObj.ids, stringObj);
}
}
newGrdpFileContent += grdpLine;
if (i < grdpFileLines.length - 1) {
newGrdpFileContent += '\n';
}
}
promises.push(writeFileAsync(grdpFilePath, newGrdpFileContent));
}
promises.push(localizationUtils.addChildGRDPFilePathsToGRD(grdpFilePathsToAdd.sort()));
await Promise.all(promises);
return true;
}
// Return true if any resources are updated
async function modifyResourcesInGRDP() {
const messagesToModify = checkLocalizedStrings.getIDSKeysToModify();
if (messagesToModify.size === 0) {
return false;
}
const grdpToMessages = mapGRDPFilePathToMessages(messagesToModify);
const promises = [];
for (const [grdpPath, messages] of grdpToMessages) {
let fileContent = await localizationUtils.parseFileContent(grdpPath);
for (const message of messages) {
const idsRegex = new RegExp(`name="${message.actualIDSKey}"`);
fileContent = fileContent.replace(idsRegex, `name="${message.ids}"`);
}
promises.push(writeFileAsync(grdpPath, fileContent));
}
await Promise.all(promises);
return true;
}
// Return true if any resources are removed
async function removeResourcesFromGRDP(keysToRemoveFromGRD) {
function indexOfFirstMatchingMessage(line, messages) {
for (let i = 0; i < messages.length; i++) {
const message = messages[i];
const match =
line.match(new RegExp(`<message[^>]*name="${message.ids}"[^>]*desc="(.*)?"[^>]*>`));
if (match) {
return i;
}
}
return -1;
}
if (keysToRemoveFromGRD.size === 0) {
return false;
}
const grdpToMessages = mapGRDPFilePathToMessages(keysToRemoveFromGRD);
const promises = [];
for (const [grdpFilePath, messages] of grdpToMessages) {
let newGrdpFileContent = '';
const grdpFileContent = await localizationUtils.parseFileContent(grdpFilePath);
const grdpFileLines = grdpFileContent.split('\n');
for (let i = 0; i < grdpFileLines.length; i++) {
const index = indexOfFirstMatchingMessage(grdpFileLines[i], messages);
if (index === -1) {
newGrdpFileContent += grdpFileLines[i];
if (i < grdpFileLines.length - 1) {
newGrdpFileContent += '\n';
}
continue;
}
messages.splice(index, 1);
while (!grdpFileLines[i].includes('</message>')) {
i++;
}
}
promises.push(writeFileAsync(grdpFilePath, newGrdpFileContent));
}
await Promise.all(promises);
return true;
}
// Given a map from IDS key to a list of messages, return a map
// from grdp file path to a list of messages with a new property
// `ids` set to the key.
function mapGRDPFilePathToMessages(keyToMessages) {
const grdpFilePathToMessages = new Map();
for (const [ids, messages] of keyToMessages) {
for (const message of messages) {
if (!grdpFilePathToMessages.has(message.grdpPath)) {
grdpFilePathToMessages.set(message.grdpPath, []);
}
message.ids = ids;
grdpFilePathToMessages.get(message.grdpPath).push(message);
}
}
return grdpFilePathToMessages;
}