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

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View 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,
};

View 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,
};
},
};

View File

@@ -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,
},
});
}
}
},
};
},
};

View File

@@ -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'}},
],
},
],
});

View 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',
});
},
};
},
};