[go: nahoru, domu]

blob: 83687d7a4dd43754ee3f5b0cb9c4f5b53b5be21e [file] [log] [blame]
// 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.
/**
* @fileoverview Rule to check ES import usage
* @author Tim van der Lippe
*/
'use strict';
const path = require('path');
const FRONT_END_DIRECTORY = path.join(__dirname, '..', '..', '..', 'front_end');
const UNITTESTS_DIRECTORY = path.join(__dirname, '..', '..', '..', 'test', 'unittests');
const INSPECTOR_OVERLAY_DIRECTORY = path.join(__dirname, '..', '..', '..', 'front_end', 'inspector_overlay');
const COMPONENT_DOCS_DIRECTORY = path.join(FRONT_END_DIRECTORY, 'ui', 'components', 'docs');
const CROSS_NAMESPACE_MESSAGE =
'Incorrect cross-namespace import: "{{importPath}}". Use "import * as Namespace from \'../namespace/namespace.js\';" instead.';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
function isStarAsImportSpecifier(specifiers) {
return specifiers.length === 1 && specifiers[0].type === 'ImportNamespaceSpecifier';
}
function isSideEffectImportSpecifier(specifiers) {
return specifiers.length === 0;
}
function isModuleEntrypoint(fileName) {
const fileNameWithoutExtension = path.basename(fileName).replace(path.extname(fileName), '');
const directoryName = path.basename(path.dirname(fileName));
// TODO(crbug.com/1011811): remove -legacy fallback
return directoryName === fileNameWithoutExtension || `${directoryName}-legacy` === fileNameWithoutExtension;
}
function computeTopLevelFolder(fileName) {
const namespaceName = path.relative(FRONT_END_DIRECTORY, fileName);
return namespaceName.substring(0, namespaceName.indexOf(path.sep));
}
function checkImportExtension(importPath, context, node) {
// import * as fs from 'fs';
if (!importPath.startsWith('.')) {
return;
}
if (!importPath.endsWith('.js') && !importPath.endsWith('.mjs')) {
context.report({
node,
message: 'Missing file extension for import "{{importPath}}"',
data: {
importPath,
},
fix(fixer) {
return fixer.replaceText(node.source, `'${importPath}.js'`);
}
});
}
}
function nodeSpecifiersSpecialImportsOnly(specifiers) {
return specifiers.length === 1 && specifiers[0].type === 'ImportSpecifier' &&
['ls', 'assertNotNull'].includes(specifiers[0].imported.name);
}
function checkStarImport(context, node, importPath, importingFileName, exportingFileName) {
if (isModuleEntrypoint(importingFileName)) {
return;
}
if (importingFileName.startsWith(COMPONENT_DOCS_DIRECTORY) &&
importPath.includes(path.join('front_end', 'helpers'))) {
return;
}
// The generated code is typically part of a different folder. Therefore,
// it is allowed to directly import these files, as they are only
// imported in 1 place at a time.
if (computeTopLevelFolder(exportingFileName) === 'generated') {
return;
}
const isSameFolder = path.dirname(importingFileName) === path.dirname(exportingFileName);
const invalidSameFolderUsage = isSameFolder && isModuleEntrypoint(exportingFileName);
const invalidCrossFolderUsage = !isSameFolder && !isModuleEntrypoint(exportingFileName);
if (invalidSameFolderUsage) {
context.report({
node,
message:
'Incorrect same-namespace import: "{{importPath}}". Use "import { Symbol } from \'./relative-file.js\';" instead.',
data: {
importPath,
},
});
}
if (invalidCrossFolderUsage) {
context.report({
node,
message: CROSS_NAMESPACE_MESSAGE,
data: {
importPath,
},
});
}
}
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'check ES import usage',
category: 'Possible Errors',
},
fixable: 'code',
schema: [] // no options
},
create: function(context) {
const importingFileName = path.resolve(context.getFilename());
return {
ExportNamedDeclaration(node) {
// Any export in a file is called an `ExportNamedDeclaration`, but
// only directly-exporting-from-import declarations have the
// `node.source` set.
if (!node.source) {
return;
}
const importPath = path.normalize(node.source.value);
checkImportExtension(importPath, context, node);
},
ImportDeclaration(node) {
checkImportExtension(node.source.value, context, node);
const importPath = path.normalize(node.source.value);
// Accidental relative URL:
// import * as Root from 'front_end/root/root.js';
//
// Should ignore named imports:
// import * as fs from 'fs';
//
// Don't use `importPath` here, as `path.normalize` removes
// the `./` from same-folder import paths.
if (!node.source.value.startsWith('.') && !/^[\w\-_]+$/.test(node.source.value)) {
context.report({
node,
message: 'Invalid relative URL import. An import should start with either "../" or "./".',
});
}
const importingFileIsUnitTestFile = importingFileName.startsWith(UNITTESTS_DIRECTORY);
const importingFileIsComponentDocsFile = importingFileName.startsWith(COMPONENT_DOCS_DIRECTORY);
if (!importingFileName.startsWith(FRONT_END_DIRECTORY) && !importingFileIsUnitTestFile) {
return;
}
if (importingFileName.startsWith(INSPECTOR_OVERLAY_DIRECTORY)) {
return;
}
if (isSideEffectImportSpecifier(node.specifiers)) {
return;
}
const exportingFileName = path.resolve(path.dirname(importingFileName), importPath);
if (importPath.includes('/front_end/') && !importingFileIsUnitTestFile && !importingFileIsComponentDocsFile) {
context.report({
node,
message:
'Invalid relative import: an import should not include the "front_end" directory. If you are in a unit test, you should import from the module entrypoint.',
});
}
if (importPath.endsWith(path.join('platform', 'platform.js')) &&
nodeSpecifiersSpecialImportsOnly(node.specifiers)) {
/* We allow direct importing of the ls and assertNotNull utility as it's so frequently used. */
return;
}
if (isStarAsImportSpecifier(node.specifiers)) {
checkStarImport(context, node, importPath, importingFileName, exportingFileName);
} else {
if (computeTopLevelFolder(importingFileName) !== computeTopLevelFolder(exportingFileName)) {
let message = CROSS_NAMESPACE_MESSAGE;
if (importPath.endsWith(path.join('common', 'ls.js'))) {
message += ' You may only import common/ls.js directly from TypeScript source files.';
}
if (importPath.includes('third_party')) {
message +=
' If the third_party dependency does not expose a single entrypoint, update es_modules_import.js to make it exempt.';
}
context.report({
node,
message,
data: {
importPath,
},
});
} else if (isModuleEntrypoint(importingFileName)) {
/**
* We allow ui/utils/utils.js to get away with this because it's not
* really a proper entry point and should be folded properly into
* the UI module, as it's exposed via `UI.Utils.X`.
* TODO * (https://crbug.com/1148274) tidy up the utils and remove this
* special case.
*/
if (importingFileName.includes(['ui', 'legacy', 'utils', 'utils.js'].join(path.sep))) {
return;
}
if (importingFileName.includes(['test_setup', 'test_setup.ts'].join(path.sep)) &&
importPath.includes([path.sep, 'helpers', path.sep].join(''))) {
/** Within test files we allow the direct import of test helpers.
* The entry point detection detects test_setup.ts as an
* entrypoint, but we don't treat it as such, it's just a file
* that Karma runs to setup the environment.
*/
return;
}
context.report({
node,
message:
'Incorrect same-namespace import: "{{importPath}}". Use "import * as File from \'./File.js\';" instead.',
data: {
importPath,
}
});
}
}
}
};
}
};