|  | 
|  | 1 | +import {isCommaToken} from '@eslint-community/eslint-utils'; | 
|  | 2 | +import {removeObjectProperty} from './fix/index.js'; | 
|  | 3 | +import {getParentheses} from './utils/index.js'; | 
|  | 4 | + | 
|  | 5 | +const MESSAGE_ID = 'require-module-attributes'; | 
|  | 6 | +const messages = { | 
|  | 7 | +	[MESSAGE_ID]: '{{type}} with empty attribute list is not allowed.', | 
|  | 8 | +}; | 
|  | 9 | + | 
|  | 10 | +const isWithToken = token => token?.type === 'Keyword' && token.value === 'with'; | 
|  | 11 | + | 
|  | 12 | +/** @param {import('eslint').Rule.RuleContext} context */ | 
|  | 13 | +const create = context => { | 
|  | 14 | +	const {sourceCode} = context; | 
|  | 15 | + | 
|  | 16 | +	context.on(['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration'], declaration => { | 
|  | 17 | +		const {source, attributes} = declaration; | 
|  | 18 | + | 
|  | 19 | +		if (!source || (Array.isArray(attributes) && attributes.length > 0)) { | 
|  | 20 | +			return; | 
|  | 21 | +		} | 
|  | 22 | + | 
|  | 23 | +		const withToken = sourceCode.getTokenAfter(source); | 
|  | 24 | + | 
|  | 25 | +		if (!isWithToken(withToken)) { | 
|  | 26 | +			return; | 
|  | 27 | +		} | 
|  | 28 | + | 
|  | 29 | +		// `WithStatement` is not possible in modules, so we don't need worry it's not attributes | 
|  | 30 | + | 
|  | 31 | +		const openingBraceToken = sourceCode.getTokenAfter(withToken); | 
|  | 32 | +		const closingBraceToken = sourceCode.getTokenAfter(openingBraceToken); | 
|  | 33 | + | 
|  | 34 | +		return { | 
|  | 35 | +			node: declaration, | 
|  | 36 | +			loc: { | 
|  | 37 | +				start: sourceCode.getLoc(openingBraceToken).start, | 
|  | 38 | +				end: sourceCode.getLoc(closingBraceToken).end, | 
|  | 39 | +			}, | 
|  | 40 | +			messageId: MESSAGE_ID, | 
|  | 41 | +			data: { | 
|  | 42 | +				type: declaration.type === 'ImportDeclaration' ? 'import statement' : 'export statement', | 
|  | 43 | +			}, | 
|  | 44 | +			/** @param {import('eslint').Rule.RuleFixer} fixer */ | 
|  | 45 | +			fix: fixer => [withToken, closingBraceToken, openingBraceToken].map(token => fixer.remove(token)), | 
|  | 46 | +		}; | 
|  | 47 | +	}); | 
|  | 48 | + | 
|  | 49 | +	context.on('ImportExpression', importExpression => { | 
|  | 50 | +		const {options: optionsNode} = importExpression; | 
|  | 51 | + | 
|  | 52 | +		if (optionsNode?.type !== 'ObjectExpression') { | 
|  | 53 | +			return; | 
|  | 54 | +		} | 
|  | 55 | + | 
|  | 56 | +		const emptyWithProperty = optionsNode.properties.find( | 
|  | 57 | +			property => | 
|  | 58 | +				property.type === 'Property' | 
|  | 59 | +				&& !property.method | 
|  | 60 | +				&& !property.shorthand | 
|  | 61 | +				&& !property.computed | 
|  | 62 | +				&& property.kind === 'init' | 
|  | 63 | +				&& ( | 
|  | 64 | +					( | 
|  | 65 | +						property.key.type === 'Identifier' | 
|  | 66 | +						&& property.key.name === 'with' | 
|  | 67 | +					) | 
|  | 68 | +					|| ( | 
|  | 69 | +						property.key.type === 'Literal' | 
|  | 70 | +						&& property.key.value === 'with' | 
|  | 71 | +					) | 
|  | 72 | +				) | 
|  | 73 | +				&& property.value.type === 'ObjectExpression' | 
|  | 74 | +				&& property.value.properties.length === 0, | 
|  | 75 | +		); | 
|  | 76 | + | 
|  | 77 | +		const nodeToRemove = optionsNode.properties.length === 0 || (emptyWithProperty && optionsNode.properties.length === 1) | 
|  | 78 | +			? optionsNode | 
|  | 79 | +			: emptyWithProperty; | 
|  | 80 | + | 
|  | 81 | +		if (!nodeToRemove) { | 
|  | 82 | +			return; | 
|  | 83 | +		} | 
|  | 84 | + | 
|  | 85 | +		const isProperty = nodeToRemove.type === 'Property'; | 
|  | 86 | + | 
|  | 87 | +		return { | 
|  | 88 | +			node: emptyWithProperty?.value ?? nodeToRemove, | 
|  | 89 | +			messageId: MESSAGE_ID, | 
|  | 90 | +			data: { | 
|  | 91 | +				type: 'import expression', | 
|  | 92 | +			}, | 
|  | 93 | +			/** @param {import('eslint').Rule.RuleFixer} fixer */ | 
|  | 94 | +			fix: fixer => isProperty | 
|  | 95 | +				? removeObjectProperty(fixer, nodeToRemove, context) | 
|  | 96 | +				: [ | 
|  | 97 | +					// Comma token before | 
|  | 98 | +					sourceCode.getTokenBefore(nodeToRemove, isCommaToken), | 
|  | 99 | +					...sourceCode.getTokens(nodeToRemove), | 
|  | 100 | +					...getParentheses(nodeToRemove, sourceCode), | 
|  | 101 | +				].map(token => fixer.remove(token)), | 
|  | 102 | +		}; | 
|  | 103 | +	}); | 
|  | 104 | +}; | 
|  | 105 | + | 
|  | 106 | +/** @type {import('eslint').Rule.RuleModule} */ | 
|  | 107 | +const config = { | 
|  | 108 | +	create, | 
|  | 109 | +	meta: { | 
|  | 110 | +		type: 'suggestion', | 
|  | 111 | +		docs: { | 
|  | 112 | +			description: 'Require non-empty module attributes for imports and exports', | 
|  | 113 | +			recommended: true, | 
|  | 114 | +		}, | 
|  | 115 | +		fixable: 'code', | 
|  | 116 | +		messages, | 
|  | 117 | +	}, | 
|  | 118 | +}; | 
|  | 119 | + | 
|  | 120 | +export default config; | 
0 commit comments