eslint-plugin-sonarjs
Version:
SonarJS rules for ESLint
170 lines (169 loc) • 6.99 kB
JavaScript
"use strict";
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
// https://sonarsource.github.io/rspec/#/rspec/S5604/javascript
Object.defineProperty(exports, "__esModule", { value: true });
exports.rule = void 0;
const index_js_1 = require("../helpers/index.js");
const meta_js_1 = require("./meta.js");
const GEOLOCATION = 'geolocation';
const CAMERA = 'camera';
const MICROPHONE = 'microphone';
const NOTIFICATIONS = 'notifications';
const PERSISTENT_STORAGE = 'persistent-storage';
const supportedPermissions = [GEOLOCATION, CAMERA, MICROPHONE, NOTIFICATIONS, PERSISTENT_STORAGE];
const DEFAULT_PERMISSIONS = [GEOLOCATION];
const messages = {
checkPermission: 'Make sure the use of the {{feature}} is necessary.',
};
exports.rule = {
meta: (0, index_js_1.generateMeta)(meta_js_1.meta, { messages, schema: meta_js_1.schema }),
create(context) {
const permissions = context.options[0]?.permissions ?? DEFAULT_PERMISSIONS;
return {
'CallExpression[callee.type="MemberExpression"]'(node) {
const call = node;
const callee = call.callee;
if (isNavigatorMemberExpression(callee, 'permissions', 'query') &&
call.arguments.length > 0) {
checkPermissions(context, call, permissions);
return;
}
if (permissions.includes(GEOLOCATION) &&
isNavigatorMemberExpression(callee, GEOLOCATION, 'watchPosition', 'getCurrentPosition')) {
context.report({
messageId: 'checkPermission',
data: {
feature: GEOLOCATION,
},
node: callee,
});
return;
}
if (isNavigatorMemberExpression(callee, 'mediaDevices', 'getUserMedia') &&
call.arguments.length > 0) {
const firstArg = (0, index_js_1.getValueOfExpression)(context, call.arguments[0], 'ObjectExpression');
checkForCameraAndMicrophonePermissions(context, permissions, callee, firstArg);
return;
}
if (permissions.includes(NOTIFICATIONS) &&
(0, index_js_1.isMemberExpression)(callee, 'Notification', 'requestPermission')) {
context.report({
messageId: 'checkPermission',
data: {
feature: NOTIFICATIONS,
},
node: callee,
});
return;
}
if (permissions.includes(PERSISTENT_STORAGE) &&
(0, index_js_1.isMemberExpression)(callee.object, 'navigator', 'storage')) {
context.report({
messageId: 'checkPermission',
data: {
feature: PERSISTENT_STORAGE,
},
node: callee,
});
}
},
NewExpression(node) {
const { callee } = node;
if (permissions.includes(NOTIFICATIONS) && (0, index_js_1.isIdentifier)(callee, 'Notification')) {
context.report({
messageId: 'checkPermission',
data: {
feature: NOTIFICATIONS,
},
node: callee,
});
}
},
};
},
};
function checkForCameraAndMicrophonePermissions(context, permissions, callee, firstArg) {
if (!firstArg) {
return;
}
const shouldCheckAudio = permissions.includes('microphone');
const shouldCheckVideo = permissions.includes(CAMERA);
if (!shouldCheckAudio && !shouldCheckVideo) {
return;
}
const perms = [];
for (const prop of firstArg.properties) {
if (prop.type === 'Property') {
const { value, key } = prop;
if ((0, index_js_1.isIdentifier)(key, 'audio') && shouldCheckAudio && isOtherThanFalse(context, value)) {
perms.push('microphone');
}
else if ((0, index_js_1.isIdentifier)(key, 'video') &&
shouldCheckVideo &&
isOtherThanFalse(context, value)) {
perms.push(CAMERA);
}
}
}
if (perms.length > 0) {
context.report({
messageId: 'checkPermission',
data: {
feature: perms.join(' and '),
},
node: callee,
});
}
}
function isOtherThanFalse(context, value) {
const exprValue = (0, index_js_1.getValueOfExpression)(context, value, 'Literal');
if (exprValue && exprValue.value === false) {
return false;
}
return true;
}
function checkPermissions(context, call, permissions) {
const firstArg = (0, index_js_1.getValueOfExpression)(context, call.arguments[0], 'ObjectExpression');
if (firstArg?.type === 'ObjectExpression') {
const nameProp = firstArg.properties.find(prop => hasNamePropertyWithPermission(prop, context, permissions));
if (nameProp) {
const { value } = nameProp.value;
context.report({
messageId: 'checkPermission',
data: {
feature: String(value),
},
node: nameProp,
});
}
}
}
function isNavigatorMemberExpression({ object, property }, firstProperty, ...secondProperty) {
return ((0, index_js_1.isMemberExpression)(object, 'navigator', firstProperty) &&
(0, index_js_1.isIdentifier)(property, ...secondProperty));
}
function hasNamePropertyWithPermission(prop, context, permissions) {
if (prop.type === 'Property' && (0, index_js_1.isIdentifier)(prop.key, 'name')) {
const value = (0, index_js_1.getValueOfExpression)(context, prop.value, 'Literal');
return (value &&
typeof value.value === 'string' &&
supportedPermissions.includes(value.value) &&
permissions.includes(value.value));
}
return false;
}