| // 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 (/profiler(\\|\/)ModuleUIStrings\.(js|ts)$/.test(filePath)) { |
| // heap_snapshot is a web worker and has its own memory space, it's strings are |
| // not displayed but serialized and sent over to the profiler to be displayed. |
| // As they are displayed on the profiler they need to be declared there. |
| continue; |
| } else if (/ModuleUIStrings\.(js|ts)$/.test(filePath)) { |
| const newFilePath = filePath.replace(/ModuleUIStrings\.(js|ts)$/, '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") ---> i18nString(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, |
| }; |