Files
upstream-mattermost/webapp/platform/eslint-plugin/rules/no-redundant-admin-config-deps.js
upstream-mirror 240db4e2a7
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05:00

440 lines
13 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/**
* Detects redundant System Console isDisabled conditions that are already
* implied by a parent config setting dependency.
*
* When setting B declares `it.stateIsFalse('A')` (or equivalent) and A is a bool
* setting that already disables itself under condition C, repeating C on B is
* redundant and can drift when A's dependencies change.
*
* Inheritance only follows bool parents: disabled bools are forced false on
* save (see getSettingValue), so stateIsFalse(parent) covers the parent's
* config checks. File/text parents can remain truthy while disabled, so their
* checks are not treated as transitive.
*
* The dependency tree is built fully before reporting. When a setting lists
* both a parent and a grandparent, redundant conditions are attributed to the
* closest (most specific) bool parent — not whichever ancestor appears first
* in the isDisabled list.
*
* Only config/state helpers are considered (stateIsFalse/True/Equals/...,
* configIsFalse/True, …). Permission and license checks are left alone —
* non-bool children still need their own permission gates.
*/
const CONFIG_HELPERS = new Set([
'stateIsFalse',
'stateIsTrue',
'stateEquals',
'stateEqualsOrDefault',
'stateMatches',
'configIsFalse',
'configIsTrue',
'configContains',
'clientConfigIsTrue',
'clientConfigIsFalse',
]);
function isItMemberCall(node, name) {
return (
node?.type === 'CallExpression' &&
node.callee?.type === 'MemberExpression' &&
!node.callee.computed &&
node.callee.object?.type === 'Identifier' &&
node.callee.object.name === 'it' &&
node.callee.property?.type === 'Identifier' &&
(name === undefined || node.callee.property.name === name)
);
}
function literalValue(node) {
if (!node) {
return undefined;
}
if (node.type === 'Literal') {
return node.value;
}
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
return node.quasis[0]?.value?.cooked;
}
return undefined;
}
function serializeArg(node) {
// Regex literals must be keyed by pattern+flags. literalValue returns a
// RegExp whose JSON.stringify is "{}", which would collapse distinct patterns.
if (node?.type === 'Literal' && node.regex) {
return `re:${JSON.stringify(node.regex.pattern)}:${JSON.stringify(node.regex.flags)}`;
}
const value = literalValue(node);
if (value === undefined) {
if (node?.type === 'Identifier') {
return `id:${node.name}`;
}
if (node?.type === 'MemberExpression') {
return `mem:${serializeArg(node.object)}.${node.property?.name ?? '?'}`;
}
if (node?.type === 'UnaryExpression' && node.operator === '!' && node.argument?.type === 'Literal') {
return `!${JSON.stringify(node.argument.value)}`;
}
return null;
}
return JSON.stringify(value);
}
function canonicalize(node) {
if (!node) {
return null;
}
if (isItMemberCall(node, 'not') && node.arguments.length === 1) {
const inner = canonicalize(node.arguments[0]);
return inner ? `not:${inner}` : null;
}
if (!isItMemberCall(node) || node.arguments.length === 0) {
return null;
}
const name = node.callee.property.name;
if (!CONFIG_HELPERS.has(name)) {
return null;
}
const args = node.arguments.map(serializeArg);
if (args.some((a) => a === null)) {
return null;
}
return `${name}:${args.join(',')}`;
}
function collectTopLevelConditions(node, out) {
if (!node) {
return;
}
if (isItMemberCall(node, 'any')) {
for (const arg of node.arguments) {
collectTopLevelConditions(arg, out);
}
return;
}
const key = canonicalize(node);
if (key) {
out.push({key, node});
}
}
function getProperty(objectExpression, name) {
if (objectExpression?.type !== 'ObjectExpression') {
return null;
}
for (const prop of objectExpression.properties) {
if (prop.type !== 'Property' || prop.computed) {
continue;
}
const keyName = getPropertyKeyName(prop.key);
if (keyName === name) {
return prop;
}
}
return null;
}
function getPropertyKeyName(keyNode) {
if (keyNode.type === 'Identifier') {
return keyNode.name;
}
if (keyNode.type === 'Literal') {
return keyNode.value;
}
return null;
}
function getSettingKey(objectExpression) {
const keyProp = getProperty(objectExpression, 'key');
if (!keyProp) {
return null;
}
return literalValue(keyProp.value);
}
function getSettingType(objectExpression) {
const typeProp = getProperty(objectExpression, 'type');
if (!typeProp) {
return null;
}
return literalValue(typeProp.value);
}
function parseSerializedStringArg(serialized) {
try {
const value = JSON.parse(serialized);
return typeof value === 'string' ? value : undefined;
} catch {
return undefined;
}
}
function dependencyParentKeys(conditions) {
const parents = [];
for (const {key} of conditions) {
// stateIsFalse:"Section.Setting" or not:stateIsTrue:"Section.Setting"
let match = (/^stateIsFalse:(.*)$/).exec(key);
if (match) {
const value = parseSerializedStringArg(match[1]);
if (value !== undefined) {
parents.push(value);
}
continue;
}
match = (/^not:stateIsTrue:(.*)$/).exec(key);
if (match) {
const value = parseSerializedStringArg(match[1]);
if (value !== undefined) {
parents.push(value);
}
}
}
return parents;
}
function isBoolSetting(key, settingsByKey) {
const defs = settingsByKey.get(key) || [];
return defs.some((def) => def.settingType === 'bool');
}
/**
* Build the full bool-parent dependency tree once, then derive inherited
* condition sets. Reporting uses this snapshot so parent attribution does not
* depend on isDisabled declaration order.
*
* @returns {Map<string, {
* isBool: boolean,
* directConditions: Set<string>,
* boolParents: string[],
* inheritedConditions: Set<string>,
* ancestors: Set<string>,
* }>}
*/
function buildDependencyTree(settingsByKey) {
const tree = new Map();
for (const [key, defs] of settingsByKey) {
const directConditions = new Set();
const parentKeys = [];
let isBool = false;
for (const def of defs) {
if (def.settingType === 'bool') {
isBool = true;
}
for (const {key: conditionKey} of def.conditions) {
directConditions.add(conditionKey);
}
for (const parentKey of def.parentKeys) {
if (!parentKeys.includes(parentKey)) {
parentKeys.push(parentKey);
}
}
}
tree.set(key, {
isBool,
directConditions,
boolParents: parentKeys.filter((parentKey) => isBoolSetting(parentKey, settingsByKey)),
inheritedConditions: new Set(),
ancestors: new Set(),
});
}
const walked = new Set();
function walk(settingKey, visiting) {
const node = tree.get(settingKey);
if (!node || walked.has(settingKey)) {
return node;
}
if (visiting.has(settingKey)) {
return node;
}
visiting.add(settingKey);
const inheritedConditions = new Set();
const ancestors = new Set();
for (const parentKey of node.boolParents) {
ancestors.add(parentKey);
const parentNode = walk(parentKey, visiting);
if (!parentNode) {
continue;
}
for (const conditionKey of parentNode.directConditions) {
inheritedConditions.add(conditionKey);
}
for (const conditionKey of parentNode.inheritedConditions) {
inheritedConditions.add(conditionKey);
}
for (const ancestorKey of parentNode.ancestors) {
ancestors.add(ancestorKey);
}
}
node.inheritedConditions = inheritedConditions;
node.ancestors = ancestors;
walked.add(settingKey);
visiting.delete(settingKey);
return node;
}
for (const key of tree.keys()) {
walk(key, new Set());
}
return tree;
}
function nodeImpliesCondition(node, conditionKey) {
return node.directConditions.has(conditionKey) || node.inheritedConditions.has(conditionKey);
}
/**
* Among immediate bool parents that imply the condition, pick the closest /
* most specific one: a parent that is a descendant of another candidate wins
* over that ancestor. Declaration order is ignored.
*/
function closestImplyingParent(settingKey, conditionKey, tree) {
const node = tree.get(settingKey);
if (!node) {
return null;
}
const candidates = node.boolParents.filter((parentKey) => {
const parentNode = tree.get(parentKey);
return parentNode && nodeImpliesCondition(parentNode, conditionKey);
});
if (candidates.length === 0) {
return null;
}
return candidates.reduce((best, candidate) => {
const bestNode = tree.get(best);
const candidateNode = tree.get(candidate);
// Prefer the candidate that is under the current best (more specific).
if (candidateNode.ancestors.has(best)) {
return candidate;
}
// Keep best when it is under the candidate.
if (bestNode.ancestors.has(candidate)) {
return best;
}
// Disjoint parents that both imply the condition: stable tie-break by key.
return candidate < best ? candidate : best;
});
}
export default {
meta: {
type: 'problem',
docs: {
description: 'Disallow System Console isDisabled conditions already implied by a parent config dependency',
},
schema: [],
messages: {
redundant:
"Redundant isDisabled condition '{{condition}}' on '{{setting}}' — already implied by dependency on '{{parent}}'. Depend on the parent setting and omit duplicated config checks.",
},
},
create(context) {
const filename = context.filename || context.getFilename();
if (!(/admin_definition/).test(filename)) {
return {};
}
const settings = [];
return {
ObjectExpression(node) {
const settingKey = getSettingKey(node);
if (typeof settingKey !== 'string' || !settingKey.includes('.')) {
return;
}
const settingType = getSettingType(node);
if (typeof settingType !== 'string') {
return;
}
const isDisabledProp = getProperty(node, 'isDisabled');
if (!isDisabledProp) {
return;
}
const conditions = [];
collectTopLevelConditions(isDisabledProp.value, conditions);
if (conditions.length === 0) {
return;
}
settings.push({
key: settingKey,
settingType,
conditions,
parentKeys: dependencyParentKeys(conditions),
node,
});
},
'Program:exit'() {
const settingsByKey = new Map();
for (const setting of settings) {
if (!settingsByKey.has(setting.key)) {
settingsByKey.set(setting.key, []);
}
settingsByKey.get(setting.key).push(setting);
}
// Build the complete tree before attributing any parent.
const tree = buildDependencyTree(settingsByKey);
for (const setting of settings) {
const node = tree.get(setting.key);
if (!node || node.boolParents.length === 0 || node.inheritedConditions.size === 0) {
continue;
}
for (const {key, node: conditionNode} of setting.conditions) {
if (!node.inheritedConditions.has(key)) {
continue;
}
const implyingParent = closestImplyingParent(setting.key, key, tree);
if (!implyingParent) {
continue;
}
context.report({
node: conditionNode,
messageId: 'redundant',
data: {
condition: key,
setting: setting.key,
parent: implyingParent,
},
});
}
}
},
};
},
};