snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
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
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
This commit is contained in:
12
webapp/platform/eslint-plugin/rules/index.js
Normal file
12
webapp/platform/eslint-plugin/rules/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import noDispatchGetState from './no-dispatch-getstate.js';
|
||||
import noRedundantAdminConfigDeps from './no-redundant-admin-config-deps.js';
|
||||
import useExternalLink from './use-external-link.js';
|
||||
|
||||
export default {
|
||||
'no-dispatch-getstate': noDispatchGetState,
|
||||
'no-redundant-admin-config-deps': noRedundantAdminConfigDeps,
|
||||
'use-external-link': useExternalLink,
|
||||
};
|
||||
49
webapp/platform/eslint-plugin/rules/no-dispatch-getstate.js
Normal file
49
webapp/platform/eslint-plugin/rules/no-dispatch-getstate.js
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
fixable: 'code',
|
||||
messages: {
|
||||
unexpected: 'Unexpected second argument passed to dispatch. getState does not need to be passed into dispatch.',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
function checkDispatch(node) {
|
||||
const args = node.arguments;
|
||||
if (args.length === 2) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'unexpected',
|
||||
fix(fixer) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
const before = sourceCode.getTokenBefore(args[1]);
|
||||
const after = sourceCode.getTokenAfter(args[1]);
|
||||
|
||||
// Remove the getState argument and the preceding comma
|
||||
const fixes = [
|
||||
fixer.remove(before),
|
||||
fixer.remove(args[1]),
|
||||
];
|
||||
|
||||
// And remove the trailing comma if one exists
|
||||
if (after.type === 'Punctuator' && after.value === ',') {
|
||||
fixes.push(fixer.remove(after));
|
||||
}
|
||||
|
||||
// Note that this sometimes mangles the whitespace slightly
|
||||
|
||||
return fixes;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"CallExpression[callee.name='dispatch']": checkDispatch,
|
||||
"CallExpression[callee.name='doDispatch']": checkDispatch,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,439 @@
|
||||
// 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,339 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {RuleTester} from 'eslint';
|
||||
|
||||
import rule from './no-redundant-admin-config-deps.js';
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
});
|
||||
|
||||
const filename = 'admin_definition.tsx';
|
||||
|
||||
ruleTester.run('no-redundant-admin-config-deps', rule, {
|
||||
valid: [
|
||||
{
|
||||
filename,
|
||||
code: `
|
||||
const AdminDefinition = {
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'EmailSettings.EnableEmailBatching',
|
||||
isDisabled: it.any(
|
||||
it.stateIsFalse('EmailSettings.SendEmailNotifications'),
|
||||
it.configIsTrue('ClusterSettings', 'Enable'),
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
key: 'EmailSettings.EmailBatchingBufferSize',
|
||||
isDisabled: it.any(
|
||||
it.stateIsFalse('EmailSettings.EnableEmailBatching'),
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
filename,
|
||||
code: `
|
||||
const AdminDefinition = {
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'ServiceSettings.EnableOAuthServiceProvider',
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource('integrations')),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'ServiceSettings.EnableDynamicClientRegistration',
|
||||
isDisabled: it.any(
|
||||
it.not(it.userHasWritePermissionOnResource('integrations')),
|
||||
it.stateIsFalse('ServiceSettings.EnableOAuthServiceProvider'),
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'ServiceSettings.DCRRedirectURIAllowlist',
|
||||
isDisabled: it.any(
|
||||
it.not(it.userHasWritePermissionOnResource('integrations')),
|
||||
it.stateIsFalse('ServiceSettings.EnableDynamicClientRegistration'),
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
||||
// Non-bool parents (fileupload) do not imply their isDisabled conditions,
|
||||
// so repeating Encrypt alongside PrivateKeyFile is intentional.
|
||||
|
||||
filename,
|
||||
code: `
|
||||
const AdminDefinition = {
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'SamlSettings.Encrypt',
|
||||
isDisabled: it.stateIsFalse('SamlSettings.Enable'),
|
||||
},
|
||||
{
|
||||
type: 'fileupload',
|
||||
key: 'SamlSettings.PrivateKeyFile',
|
||||
isDisabled: it.stateIsFalse('SamlSettings.Encrypt'),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'SamlSettings.SignRequest',
|
||||
isDisabled: it.any(
|
||||
it.stateIsFalse('SamlSettings.Encrypt'),
|
||||
it.stateIsFalse('SamlSettings.PrivateKeyFile'),
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
||||
// Distinct regex patterns on the same helper+key are not the same condition.
|
||||
|
||||
filename,
|
||||
code: `
|
||||
const AdminDefinition = {
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'SupportSettings.Enable',
|
||||
isDisabled: it.stateMatches('SupportSettings.ReportAProblemType', /link/),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'SupportSettings.Mail',
|
||||
isDisabled: it.any(
|
||||
it.stateIsFalse('SupportSettings.Enable'),
|
||||
it.stateMatches('SupportSettings.ReportAProblemType', /email/),
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
||||
// Identifier parent keys must not throw during canonicalize/JSON.parse.
|
||||
|
||||
filename,
|
||||
code: `
|
||||
const PARENT_KEY = 'ServiceSettings.EnableOAuthServiceProvider';
|
||||
const AdminDefinition = {
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'ServiceSettings.EnableDynamicClientRegistration',
|
||||
isDisabled: it.stateIsFalse(PARENT_KEY),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'ServiceSettings.DCRRedirectURIAllowlist',
|
||||
isDisabled: it.stateIsFalse('ServiceSettings.EnableDynamicClientRegistration'),
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
||||
// Non-admin_definition files are ignored
|
||||
|
||||
filename: 'other_file.tsx',
|
||||
code: `
|
||||
const x = {
|
||||
type: 'number',
|
||||
key: 'EmailSettings.EmailBatchingBufferSize',
|
||||
isDisabled: it.any(
|
||||
it.stateIsFalse('EmailSettings.SendEmailNotifications'),
|
||||
it.stateIsFalse('EmailSettings.EnableEmailBatching'),
|
||||
),
|
||||
};
|
||||
const y = {
|
||||
type: 'bool',
|
||||
key: 'EmailSettings.EnableEmailBatching',
|
||||
isDisabled: it.stateIsFalse('EmailSettings.SendEmailNotifications'),
|
||||
};
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
filename,
|
||||
code: `
|
||||
const AdminDefinition = {
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'EmailSettings.EnableEmailBatching',
|
||||
isDisabled: it.any(
|
||||
it.stateIsFalse('EmailSettings.SendEmailNotifications'),
|
||||
it.configIsTrue('ClusterSettings', 'Enable'),
|
||||
it.configIsFalse('ServiceSettings', 'SiteURL'),
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
key: 'EmailSettings.EmailBatchingBufferSize',
|
||||
isDisabled: it.any(
|
||||
it.stateIsFalse('EmailSettings.SendEmailNotifications'),
|
||||
it.stateIsFalse('EmailSettings.EnableEmailBatching'),
|
||||
it.configIsTrue('ClusterSettings', 'Enable'),
|
||||
it.configIsFalse('ServiceSettings', 'SiteURL'),
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
errors: [
|
||||
{messageId: 'redundant', data: {condition: 'stateIsFalse:"EmailSettings.SendEmailNotifications"', setting: 'EmailSettings.EmailBatchingBufferSize', parent: 'EmailSettings.EnableEmailBatching'}},
|
||||
{messageId: 'redundant', data: {condition: 'configIsTrue:"ClusterSettings","Enable"', setting: 'EmailSettings.EmailBatchingBufferSize', parent: 'EmailSettings.EnableEmailBatching'}},
|
||||
{messageId: 'redundant', data: {condition: 'configIsFalse:"ServiceSettings","SiteURL"', setting: 'EmailSettings.EmailBatchingBufferSize', parent: 'EmailSettings.EnableEmailBatching'}},
|
||||
],
|
||||
},
|
||||
{
|
||||
filename,
|
||||
code: `
|
||||
const AdminDefinition = {
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'ServiceSettings.EnableOAuthServiceProvider',
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource('integrations')),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'ServiceSettings.EnableDynamicClientRegistration',
|
||||
isDisabled: it.any(
|
||||
it.not(it.userHasWritePermissionOnResource('integrations')),
|
||||
it.stateIsFalse('ServiceSettings.EnableOAuthServiceProvider'),
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'ServiceSettings.DCRRedirectURIAllowlist',
|
||||
isDisabled: it.any(
|
||||
it.not(it.userHasWritePermissionOnResource('integrations')),
|
||||
it.stateIsFalse('ServiceSettings.EnableOAuthServiceProvider'),
|
||||
it.stateIsFalse('ServiceSettings.EnableDynamicClientRegistration'),
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
errors: [
|
||||
{messageId: 'redundant', data: {condition: 'stateIsFalse:"ServiceSettings.EnableOAuthServiceProvider"', setting: 'ServiceSettings.DCRRedirectURIAllowlist', parent: 'ServiceSettings.EnableDynamicClientRegistration'}},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
// Bool parent Encrypt implies Enable; PrivateKeyFile should not repeat Enable
|
||||
|
||||
filename,
|
||||
code: `
|
||||
const AdminDefinition = {
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'SamlSettings.Encrypt',
|
||||
isDisabled: it.stateIsFalse('SamlSettings.Enable'),
|
||||
},
|
||||
{
|
||||
type: 'fileupload',
|
||||
key: 'SamlSettings.PrivateKeyFile',
|
||||
isDisabled: it.any(
|
||||
it.stateIsFalse('SamlSettings.Enable'),
|
||||
it.stateIsFalse('SamlSettings.Encrypt'),
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
errors: [
|
||||
{messageId: 'redundant', data: {condition: 'stateIsFalse:"SamlSettings.Enable"', setting: 'SamlSettings.PrivateKeyFile', parent: 'SamlSettings.Encrypt'}},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
// Grandparent listed before parent: still attribute to the closest parent (Mid),
|
||||
// not Root, after the full tree is built.
|
||||
|
||||
filename,
|
||||
code: `
|
||||
const AdminDefinition = {
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'FeatureSettings.Root',
|
||||
isDisabled: it.configIsTrue('ClusterSettings', 'Enable'),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'FeatureSettings.Mid',
|
||||
isDisabled: it.any(
|
||||
it.stateIsFalse('FeatureSettings.Root'),
|
||||
it.configIsTrue('ClusterSettings', 'Enable'),
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
key: 'FeatureSettings.Leaf',
|
||||
isDisabled: it.any(
|
||||
it.stateIsFalse('FeatureSettings.Root'),
|
||||
it.stateIsFalse('FeatureSettings.Mid'),
|
||||
it.configIsTrue('ClusterSettings', 'Enable'),
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
errors: [
|
||||
{messageId: 'redundant', data: {condition: 'configIsTrue:"ClusterSettings","Enable"', setting: 'FeatureSettings.Mid', parent: 'FeatureSettings.Root'}},
|
||||
{messageId: 'redundant', data: {condition: 'stateIsFalse:"FeatureSettings.Root"', setting: 'FeatureSettings.Leaf', parent: 'FeatureSettings.Mid'}},
|
||||
{messageId: 'redundant', data: {condition: 'configIsTrue:"ClusterSettings","Enable"', setting: 'FeatureSettings.Leaf', parent: 'FeatureSettings.Mid'}},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
// Same regex on parent and child is redundant; distinct patterns are not.
|
||||
|
||||
filename,
|
||||
code: `
|
||||
const AdminDefinition = {
|
||||
settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'SupportSettings.Enable',
|
||||
isDisabled: it.stateMatches('SupportSettings.ReportAProblemType', /link/),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'SupportSettings.Mail',
|
||||
isDisabled: it.any(
|
||||
it.stateIsFalse('SupportSettings.Enable'),
|
||||
it.stateMatches('SupportSettings.ReportAProblemType', /link/),
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
`,
|
||||
errors: [
|
||||
{messageId: 'redundant', data: {condition: 'stateMatches:"SupportSettings.ReportAProblemType",re:"link":""', setting: 'SupportSettings.Mail', parent: 'SupportSettings.Enable'}},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
41
webapp/platform/eslint-plugin/rules/use-external-link.js
Normal file
41
webapp/platform/eslint-plugin/rules/use-external-link.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import astUtils from 'jsx-ast-utils';
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce all anchors with target="_blank" to use ExternalLink component',
|
||||
},
|
||||
},
|
||||
create: (context) => {
|
||||
return {
|
||||
JSXOpeningElement: (node) => {
|
||||
const typeCheck = 'a';
|
||||
const nodeType = astUtils.elementType(node);
|
||||
|
||||
// Only check anchor elements
|
||||
if (!nodeType || typeCheck !== nodeType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propsToValidate = ['target'];
|
||||
const values = propsToValidate.map((prop) => astUtils.getPropValue(astUtils.getProp(node.attributes, prop)));
|
||||
|
||||
// Checks if the target attribute is set to _blank (ie, is an external link)
|
||||
const hasBlankTarget = values.some((value) => value != null && value === '_blank');
|
||||
|
||||
// When there is no target value at all, this rule does not apply:
|
||||
if (!hasBlankTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
message: 'Use ExternalLink component (components/external_link) for _blank target link-outs',
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user