UNPKG

xo

Version:

JavaScript/TypeScript linter (ESLint wrapper) with great defaults

386 lines 11.3 kB
const isPropertyContainer = (value) => typeof value === 'function' || (typeof value === 'object' && value !== null); const isIdentifierNode = (node) => node.type === 'Identifier' && 'name' in node && typeof node.name === 'string'; const isLiteralNode = (node) => node.type === 'Literal'; const isMemberExpressionNode = (node) => node.type === 'MemberExpression' && 'computed' in node && typeof node.computed === 'boolean' && 'object' in node && 'property' in node; const isBinaryExpressionNode = (node) => node.type === 'BinaryExpression' && 'operator' in node && typeof node.operator === 'string' && 'left' in node && 'right' in node; const isNewExpressionNode = (node) => node.type === 'NewExpression' && 'callee' in node; const createPropertyInfo = (value, extraProperties = []) => { const all = new Set(); const callable = new Set(); for (const propertyName of extraProperties) { all.add(propertyName); } for (let currentValue = value; isPropertyContainer(currentValue); currentValue = Object.getPrototypeOf(currentValue)) { for (const propertyName of Object.getOwnPropertyNames(currentValue)) { all.add(propertyName); const descriptor = Object.getOwnPropertyDescriptor(currentValue, propertyName); if (typeof descriptor?.value === 'function') { callable.add(propertyName); } } } return { all, callable, }; }; const emptyFunction = () => undefined; // Keep the checked native object list explicit so rule behavior stays predictable. const nativeObjectDefinitions = [ { typeName: 'Array', instance: [], static: Array, prototype: Array.prototype, }, { typeName: 'ArrayBuffer', instance: new ArrayBuffer(0), static: ArrayBuffer, prototype: ArrayBuffer.prototype, }, { typeName: 'Boolean', instance: Boolean.prototype, static: Boolean, prototype: Boolean.prototype, }, { typeName: 'DataView', instance: new DataView(new ArrayBuffer(1)), static: DataView, prototype: DataView.prototype, }, { typeName: 'Date', instance: new Date(), static: Date, prototype: Date.prototype, }, { typeName: 'Float32Array', instance: new Float32Array(), static: Float32Array, prototype: Float32Array.prototype, }, { typeName: 'Float64Array', instance: new Float64Array(), static: Float64Array, prototype: Float64Array.prototype, }, { typeName: 'Function', instance: emptyFunction, static: Function, prototype: Function.prototype, }, { typeName: 'Int8Array', instance: new Int8Array(), static: Int8Array, prototype: Int8Array.prototype, }, { typeName: 'Int16Array', instance: new Int16Array(), static: Int16Array, prototype: Int16Array.prototype, }, { typeName: 'Int32Array', instance: new Int32Array(), static: Int32Array, prototype: Int32Array.prototype, }, { typeName: 'Map', instance: new Map(), static: Map, prototype: Map.prototype, }, { typeName: 'Number', instance: Number.prototype, static: Number, prototype: Number.prototype, }, { typeName: 'Object', instance: {}, static: Object, prototype: Object.prototype, }, { typeName: 'Promise', instance: Promise.resolve(), static: Promise, prototype: Promise.prototype, }, { typeName: 'RegExp', instance: /./v, static: RegExp, prototype: RegExp.prototype, }, { typeName: 'Set', instance: new Set(), static: Set, prototype: Set.prototype, }, { typeName: 'String', instance: String.prototype, instanceProperties: ['length'], static: String, prototype: String.prototype, }, { typeName: 'Uint8Array', instance: new Uint8Array(), static: Uint8Array, prototype: Uint8Array.prototype, }, { typeName: 'Uint8ClampedArray', instance: new Uint8ClampedArray(), static: Uint8ClampedArray, prototype: Uint8ClampedArray.prototype, }, { typeName: 'Uint16Array', instance: new Uint16Array(), static: Uint16Array, prototype: Uint16Array.prototype, }, { typeName: 'Uint32Array', instance: new Uint32Array(), static: Uint32Array, prototype: Uint32Array.prototype, }, { typeName: 'URL', instance: new URL('https://example.com'), static: URL, prototype: URL.prototype, }, { typeName: 'URLSearchParams', instance: new URLSearchParams(), static: URLSearchParams, prototype: URLSearchParams.prototype, }, { typeName: 'JSON', static: JSON, }, { typeName: 'Math', static: Math, }, { typeName: 'Reflect', static: Reflect, }, ]; const nativeObjects = new Map(); for (const nativeObjectDefinition of nativeObjectDefinitions) { const nativeObjectInfo = {}; if (nativeObjectDefinition.instance) { nativeObjectInfo.instance = createPropertyInfo(nativeObjectDefinition.instance, nativeObjectDefinition.instanceProperties); } if (nativeObjectDefinition.prototype) { nativeObjectInfo.prototype = createPropertyInfo(nativeObjectDefinition.prototype); } if (nativeObjectDefinition.static) { nativeObjectInfo.static = createPropertyInfo(nativeObjectDefinition.static); } nativeObjects.set(nativeObjectDefinition.typeName, nativeObjectInfo); } const getPropertyName = (memberExpression) => { const { property } = memberExpression; if (memberExpression.computed) { return isLiteralNode(property) && typeof property.value === 'string' ? property.value : undefined; } return isIdentifierNode(property) ? property.name : undefined; }; const resolveBinaryExpressionType = (binaryExpression) => { if (binaryExpression.operator !== '+') { return undefined; } const leftReference = resolveNativeObjectReference(binaryExpression.left); const rightReference = resolveNativeObjectReference(binaryExpression.right); if (leftReference?.usage !== 'instance' || rightReference?.usage !== 'instance') { return undefined; } if (leftReference.typeName === 'String' || rightReference.typeName === 'String') { return { typeName: 'String', usage: 'instance', }; } return undefined; }; const resolveIdentifierReference = (node) => { if (!nativeObjects.has(node.name)) { return undefined; } return { typeName: node.name, usage: 'static', }; }; const resolveLiteralReference = (node) => { if (node.regex) { return { typeName: 'RegExp', usage: 'instance', }; } if (typeof node.value === 'boolean') { return { typeName: 'Boolean', usage: 'instance', }; } if (typeof node.value === 'number') { return { typeName: 'Number', usage: 'instance', }; } if (typeof node.value === 'string') { return { typeName: 'String', usage: 'instance', }; } return undefined; }; const resolvePrototypeReference = (node) => { if (getPropertyName(node) !== 'prototype' || !isIdentifierNode(node.object) || !nativeObjects.has(node.object.name)) { return undefined; } return { typeName: node.object.name, usage: 'prototype', }; }; const resolveNewExpressionReference = (node) => { if (!isIdentifierNode(node.callee) || !nativeObjects.has(node.callee.name)) { return undefined; } return { typeName: node.callee.name, usage: 'instance', }; }; function resolveNativeObjectReference(node) { if (!node) { return undefined; } if (isMemberExpressionNode(node)) { return resolvePrototypeReference(node); } if (isBinaryExpressionNode(node)) { return resolveBinaryExpressionType(node); } if (isLiteralNode(node)) { return resolveLiteralReference(node); } if (isIdentifierNode(node)) { return resolveIdentifierReference(node); } if (isNewExpressionNode(node)) { return resolveNewExpressionReference(node); } switch (node.type) { case 'ArrayExpression': { return { typeName: 'Array', usage: 'instance', }; } case 'ArrowFunctionExpression': case 'FunctionExpression': { return { typeName: 'Function', usage: 'instance', }; } case 'ObjectExpression': { return { typeName: 'Object', usage: 'instance', }; } case 'TemplateLiteral': { return { typeName: 'String', usage: 'instance', }; } default: { return undefined; } } } const noUseExtendNativeRule = { meta: { type: 'problem', docs: { description: 'Disallow relying on non-standard properties on native objects', }, messages: { unexpected: 'Avoid relying on extended native objects.', }, schema: [], }, create(context) { return { // eslint-disable-next-line @typescript-eslint/naming-convention MemberExpression(node) { const propertyName = getPropertyName(node); if (!propertyName) { return; } const nativeObjectReference = resolveNativeObjectReference(node.object); if (!nativeObjectReference) { return; } const propertyInfo = nativeObjects.get(nativeObjectReference.typeName)?.[nativeObjectReference.usage]; if (!propertyInfo) { return; } const isCall = node.parent.type === 'CallExpression' && node.parent.callee === node; if (isCall) { if (!propertyInfo.callable.has(propertyName)) { context.report({ node, messageId: 'unexpected', }); } return; } if (!propertyInfo.all.has(propertyName)) { context.report({ node, messageId: 'unexpected', }); } }, }; }, }; export default noUseExtendNativeRule; //# sourceMappingURL=no-use-extend-native.js.map