Skip to content

[code-infra] Add plugin to check for index file access #46178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,24 @@ module.exports = /** @type {Config} */ ({
'id-denylist': ['error', 'e'],
},
overrides: [
...['mui-material', 'mui-system', 'mui-utils', 'mui-lab', 'mui-utils', 'mui-styled-engine'].map(
(packageName) => ({
files: [`packages/${packageName}/src/**/*.?(c|m)[jt]s?(x)`],
excludedFiles: ['*.test.*', '*.spec.*'],
rules: {
'material-ui/no-restricted-resolved-imports': [
'error',
[
{
pattern: `*/packages/${packageName}/src/index.*`,
message:
"Don't import from the package index. Import the specific module directly instead.",
},
],
],
},
}),
),
{
files: [
// matching the pattern of the test runner
Expand Down
3 changes: 2 additions & 1 deletion packages/eslint-plugin-material-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"description": "Custom eslint rules for Material UI.",
"main": "src/index.js",
"dependencies": {
"emoji-regex": "^10.4.0"
"emoji-regex": "^10.4.0",
"eslint-module-utils": "^2.12.0"
},
"devDependencies": {
"@types/eslint": "^8.56.12",
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin-material-ui/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ module.exports.rules = {
'no-styled-box': require('./rules/no-styled-box'),
'straight-quotes': require('./rules/straight-quotes'),
'disallow-react-api-in-server-components': require('./rules/disallow-react-api-in-server-components'),
'no-restricted-resolved-imports': require('./rules/no-restricted-resolved-imports'),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Main package entry point
export { default as Button } from './components/Button';
export { default as TextField } from './components/TextField';
export { default as capitalize } from './utils/capitalize';
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
const path = require('path');
const resolve = require('eslint-module-utils/resolve').default;
const moduleVisitor = require('eslint-module-utils/moduleVisitor').default;
/**
* @typedef {Object} PatternConfig
* @property {string} pattern - The pattern to match against resolved imports
* @property {string} [message] - Custom message to show when the pattern matches
*/

/**
* Creates an ESLint rule that restricts imports based on their resolved paths.
* Works with both ESM (import) and CommonJS (require) imports.
*
* @type {import('eslint').Rule.RuleModule}
*/
const rule = {
meta: {
docs: {
description: 'Disallow imports that resolve to certain patterns.',
},
messages: {
restrictedResolvedImport:
'Importing from "{{importSource}}" is restricted because it resolves to "{{resolvedPath}}", which matches the pattern "{{pattern}}".{{customMessage}}',
},
type: 'suggestion',
schema: [
{
type: 'array',
items: {
type: 'object',
properties: {
pattern: { type: 'string' },
message: { type: 'string' },
},
required: ['pattern'],
additionalProperties: false,
},
},
],
},
create(context) {
const options = context.options[0] || [];

if (!Array.isArray(options) || options.length === 0) {
return {};
}

return moduleVisitor(
(source, node) => {
// Get the resolved path of the import
const resolvedPath = resolve(source.value, context);

if (!resolvedPath) {
return;
}

// Normalize the resolved path to use forward slashes
const normalizedPath = resolvedPath.split(path.sep).join('/');

// Check each pattern against the resolved path
for (const option of options) {
const { pattern, message = '' } = option;

// Convert the pattern to a regex
// Escape special characters and convert * to .*
const regexPattern = new RegExp(
pattern
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape special regex chars except *
.replace(/\*/g, '.*'), // Convert * to .* for wildcard matching
);

if (regexPattern.test(normalizedPath)) {
context.report({
node,
messageId: 'restrictedResolvedImport',
data: {
importSource: source.value,
resolvedPath: normalizedPath,
pattern,
customMessage: message ? ` ${message}` : '',
},
});

// Stop after first match
break;
}
}
},
{ commonjs: true, es6: true },
); // This handles both require() and import statements
},
};

module.exports = rule;
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const eslint = require('eslint');
const path = require('path');
const rule = require('./no-restricted-resolved-imports');

// Get absolute paths for our fixtures
const fixturesDir = path.resolve(__dirname, './__fixtures__/no-restricted-resolved-imports');
const mockPackageDir = path.join(fixturesDir, 'mock-package');
const badFilePath = path.join(mockPackageDir, 'src/components/ButtonGroup/index.js');
const goodFilePath = path.join(mockPackageDir, 'src/components/GoodExample/index.js');

// Create a custom rule tester with the fixture's ESLint configuration
const ruleTester = new eslint.RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
paths: [path.join(mockPackageDir, 'src')],
moduleDirectory: ['node_modules', path.join(mockPackageDir, 'src')],
},
},
},
});

// ESLint requires the files to actually exist for the resolver to work
// So we're using real files in the test fixtures
ruleTester.run('no-restricted-resolved-imports', rule, {
valid: [
// No options provided - rule shouldn't apply
{
code: "import { Button } from '../../index';",
filename: badFilePath,
options: [],
},
// Empty options array - rule shouldn't apply
{
code: "import { Button } from '../../index';",
filename: badFilePath,
options: [[]],
},
// Good example - importing from the component directly
{
code: "import Button from '../Button';",
filename: goodFilePath,
options: [
[
{
pattern: '*/mock-package/src/index.js',
message: 'Import the specific module directly instead of from the package index.',
},
],
],
},
],
invalid: [
// Bad example - importing from the package index
{
code: "import { Button } from '../../index';",
filename: badFilePath,
options: [
[
{
pattern: '*/mock-package/src/index.js',
message: 'Import the specific module directly instead of from the package index.',
},
],
],
errors: [
{
messageId: 'restrictedResolvedImport',
},
],
},
],
});
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading