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:
203
webapp/platform/eslint-plugin/LICENSE.txt
Normal file
203
webapp/platform/eslint-plugin/LICENSE.txt
Normal file
@@ -0,0 +1,203 @@
|
||||
Copyright 2015-present Mattermost, Inc.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
103
webapp/platform/eslint-plugin/README.md
Normal file
103
webapp/platform/eslint-plugin/README.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# @mattermost/eslint-plugin
|
||||
|
||||
An ESLint plugin containing the configuration used by Mattermost as well as support for custom rules specific to the Mattermost code base.
|
||||
|
||||
## Custom Rules
|
||||
|
||||
### no-dispatch-getstate
|
||||
|
||||
Prevents passing a [redux](https://redux.js.org/) store's `getState` into its `dispatch` as an unnecessary second argument.
|
||||
|
||||
We started doing this accidentally at some point because of a misunderstanding about how [redux-thunk](https://github.com/reduxjs/redux-thunk) worked, so this stops anyone from making that same mistake again.
|
||||
|
||||
Examples of **incorrect** code for this rule:
|
||||
```javascript
|
||||
export function someAction() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(doSomething(), getState);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Examples of **correct** code for this rule:
|
||||
```javascript
|
||||
export function someAction() {
|
||||
return (dispatch) => {
|
||||
dispatch(doSomething());
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### no-redundant-admin-config-deps
|
||||
|
||||
Enforces that System Console settings in `admin_definition` files do not repeat `isDisabled` config/state checks already implied by a parent setting they depend on.
|
||||
|
||||
When setting B disables itself with `it.stateIsFalse('A')`, A is a bool setting, and A already includes condition C (for example `it.configIsTrue('ClusterSettings', 'Enable')`), repeating C on B is redundant and tends to drift. Keep the dependency on A and omit the duplicated checks. Inheritance only follows bool parents (disabled bools are forced false on save). Permission and license helpers are not treated as config dependencies.
|
||||
|
||||
The rule builds the full bool-parent dependency tree before reporting. If a setting lists both a parent and a grandparent, redundant conditions are attributed to the closest (most specific) parent — not whichever ancestor appears first in `isDisabled`.
|
||||
|
||||
Examples of **incorrect** code for this rule:
|
||||
```javascript
|
||||
{
|
||||
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.SendEmailNotifications'),
|
||||
it.stateIsFalse('EmailSettings.EnableEmailBatching'),
|
||||
it.configIsTrue('ClusterSettings', 'Enable'),
|
||||
),
|
||||
},
|
||||
```
|
||||
|
||||
Examples of **correct** code for this rule:
|
||||
```javascript
|
||||
{
|
||||
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'),
|
||||
),
|
||||
},
|
||||
```
|
||||
|
||||
### use-external-link
|
||||
|
||||
Ensures that any link which opens a URL outside of Mattermost using `target="_blank"` uses the `ExternalLink` component.
|
||||
|
||||
Examples of **incorrect** code for this rule:
|
||||
```javascript
|
||||
export function SomeLink() {
|
||||
return (
|
||||
<a
|
||||
href="https://example.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Examples of **correct** code for this rule:
|
||||
```javascript
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
export function SomeLink() {
|
||||
return <ExternalLink href="https://example.com"/>;
|
||||
}
|
||||
```
|
||||
636
webapp/platform/eslint-plugin/configs/base.js
Normal file
636
webapp/platform/eslint-plugin/configs/base.js
Normal file
@@ -0,0 +1,636 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import path from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
|
||||
import {FlatCompat} from '@eslint/eslintrc';
|
||||
import js from '@eslint/js';
|
||||
import stylisticPlugin from '@stylistic/eslint-plugin';
|
||||
import typescriptPlugin from '@typescript-eslint/eslint-plugin';
|
||||
import typescriptParser from '@typescript-eslint/parser';
|
||||
import headersPlugin from 'eslint-plugin-headers';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import globals from 'globals';
|
||||
|
||||
import rules from '../rules/index.js';
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: dirname,
|
||||
});
|
||||
|
||||
const base = {
|
||||
files: ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 8,
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
...globals.es6,
|
||||
},
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
impliedStrict: true,
|
||||
modules: true,
|
||||
},
|
||||
},
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: {
|
||||
'@mattermost': {
|
||||
rules,
|
||||
},
|
||||
'@stylistic': stylisticPlugin,
|
||||
typescript: typescriptPlugin,
|
||||
headers: headersPlugin,
|
||||
import: importPlugin,
|
||||
},
|
||||
rules: {
|
||||
'@mattermost/no-dispatch-getstate': 2,
|
||||
'@mattermost/no-redundant-admin-config-deps': 2,
|
||||
'@mattermost/use-external-link': 2,
|
||||
'@stylistic/array-bracket-spacing': [
|
||||
2,
|
||||
'never',
|
||||
],
|
||||
'@stylistic/arrow-parens': [
|
||||
2,
|
||||
'always',
|
||||
],
|
||||
'@stylistic/arrow-spacing': [
|
||||
2,
|
||||
{
|
||||
before: true,
|
||||
after: true,
|
||||
},
|
||||
],
|
||||
'@stylistic/brace-style': [
|
||||
2,
|
||||
'1tbs',
|
||||
{
|
||||
allowSingleLine: false,
|
||||
},
|
||||
],
|
||||
'@stylistic/comma-dangle': [
|
||||
2,
|
||||
'always-multiline',
|
||||
],
|
||||
'@stylistic/comma-spacing': [
|
||||
2,
|
||||
{
|
||||
before: false,
|
||||
after: true,
|
||||
},
|
||||
],
|
||||
'@stylistic/comma-style': [
|
||||
2,
|
||||
'last',
|
||||
],
|
||||
'@stylistic/computed-property-spacing': [
|
||||
2,
|
||||
'never',
|
||||
],
|
||||
'@stylistic/dot-location': [
|
||||
2,
|
||||
'object',
|
||||
],
|
||||
'@stylistic/eol-last': ['error', 'always'],
|
||||
'@stylistic/function-call-spacing': [
|
||||
2,
|
||||
'never',
|
||||
],
|
||||
'@stylistic/generator-star-spacing': [
|
||||
2,
|
||||
{
|
||||
before: false,
|
||||
after: true,
|
||||
},
|
||||
],
|
||||
'@stylistic/indent': [
|
||||
2,
|
||||
4,
|
||||
{
|
||||
SwitchCase: 0,
|
||||
},
|
||||
],
|
||||
'@stylistic/jsx-quotes': [
|
||||
2,
|
||||
'prefer-single',
|
||||
],
|
||||
'@stylistic/key-spacing': [
|
||||
2,
|
||||
{
|
||||
beforeColon: false,
|
||||
afterColon: true,
|
||||
mode: 'strict',
|
||||
},
|
||||
],
|
||||
'@stylistic/keyword-spacing': [
|
||||
2,
|
||||
{
|
||||
before: true,
|
||||
after: true,
|
||||
overrides: {},
|
||||
},
|
||||
],
|
||||
'@stylistic/linebreak-style': 2,
|
||||
'@stylistic/lines-around-comment': [
|
||||
2,
|
||||
{
|
||||
beforeBlockComment: true,
|
||||
beforeLineComment: true,
|
||||
allowBlockStart: true,
|
||||
allowBlockEnd: true,
|
||||
},
|
||||
],
|
||||
'@stylistic/max-statements-per-line': [
|
||||
2,
|
||||
{
|
||||
max: 1,
|
||||
},
|
||||
],
|
||||
'@stylistic/member-delimiter-style': 2,
|
||||
'@stylistic/multiline-ternary': [
|
||||
1,
|
||||
'never',
|
||||
],
|
||||
'@stylistic/new-parens': 2,
|
||||
'@stylistic/no-confusing-arrow': 2,
|
||||
'@stylistic/no-extra-parens': 0,
|
||||
'@stylistic/no-extra-semi': 2,
|
||||
'@stylistic/no-floating-decimal': 2,
|
||||
'@stylistic/no-mixed-operators': [
|
||||
2,
|
||||
{
|
||||
allowSamePrecedence: false,
|
||||
},
|
||||
],
|
||||
'@stylistic/no-mixed-spaces-and-tabs': 2,
|
||||
'@stylistic/no-multi-spaces': [
|
||||
2,
|
||||
{
|
||||
exceptions: {
|
||||
Property: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
'@stylistic/no-multiple-empty-lines': [
|
||||
2,
|
||||
{
|
||||
max: 1,
|
||||
},
|
||||
],
|
||||
'@stylistic/no-tabs': 0,
|
||||
'@stylistic/no-trailing-spaces': [
|
||||
2,
|
||||
{
|
||||
skipBlankLines: false,
|
||||
},
|
||||
],
|
||||
'@stylistic/no-whitespace-before-property': 2,
|
||||
'@stylistic/object-curly-newline': 0,
|
||||
'@stylistic/object-curly-spacing': [
|
||||
2,
|
||||
'never',
|
||||
],
|
||||
'@stylistic/object-property-newline': [
|
||||
2,
|
||||
{
|
||||
allowAllPropertiesOnSameLine: true,
|
||||
},
|
||||
],
|
||||
'@stylistic/one-var-declaration-per-line': 0,
|
||||
'@stylistic/operator-linebreak': [
|
||||
2,
|
||||
'after',
|
||||
],
|
||||
'@stylistic/padded-blocks': [
|
||||
2,
|
||||
'never',
|
||||
],
|
||||
'@stylistic/quote-props': [
|
||||
2,
|
||||
'as-needed',
|
||||
],
|
||||
'@stylistic/quotes': [
|
||||
2,
|
||||
'single',
|
||||
{
|
||||
avoidEscape: true,
|
||||
},
|
||||
],
|
||||
'@stylistic/rest-spread-spacing': [
|
||||
2,
|
||||
'never',
|
||||
],
|
||||
'@stylistic/semi': [
|
||||
2,
|
||||
'always',
|
||||
],
|
||||
'@stylistic/semi-spacing': [
|
||||
2,
|
||||
{
|
||||
before: false,
|
||||
after: true,
|
||||
},
|
||||
],
|
||||
'@stylistic/space-before-blocks': [
|
||||
2,
|
||||
'always',
|
||||
],
|
||||
'@stylistic/space-before-function-paren': [
|
||||
2,
|
||||
{
|
||||
anonymous: 'never',
|
||||
named: 'never',
|
||||
asyncArrow: 'always',
|
||||
},
|
||||
],
|
||||
'@stylistic/space-in-parens': [
|
||||
2,
|
||||
'never',
|
||||
],
|
||||
'@stylistic/space-infix-ops': 2,
|
||||
'@stylistic/space-unary-ops': [
|
||||
2,
|
||||
{
|
||||
words: true,
|
||||
nonwords: false,
|
||||
},
|
||||
],
|
||||
'@stylistic/template-curly-spacing': [
|
||||
2,
|
||||
'never',
|
||||
],
|
||||
'@stylistic/type-annotation-spacing': 2,
|
||||
'@stylistic/wrap-iife': [
|
||||
2,
|
||||
'outside',
|
||||
],
|
||||
'@stylistic/wrap-regex': 2,
|
||||
'@typescript-eslint/array-type': [2, {default: 'array-simple'}],
|
||||
'@typescript-eslint/consistent-type-imports': ['error', {disallowTypeAnnotations: false}],
|
||||
'@typescript-eslint/explicit-function-return-type': 0,
|
||||
'@typescript-eslint/explicit-module-boundary-types': 0,
|
||||
'@typescript-eslint/naming-convention': [
|
||||
2,
|
||||
{
|
||||
selector: 'function',
|
||||
format: ['camelCase', 'PascalCase'],
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
|
||||
},
|
||||
{
|
||||
selector: 'parameter',
|
||||
format: ['camelCase', 'PascalCase'],
|
||||
leadingUnderscore: 'allow',
|
||||
},
|
||||
{
|
||||
selector: 'typeLike',
|
||||
format: ['PascalCase'],
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-dupe-class-members': 2,
|
||||
'@typescript-eslint/no-empty-function': 0,
|
||||
'@typescript-eslint/no-empty-object-type': [
|
||||
2,
|
||||
{
|
||||
allowInterfaces: 'with-single-extends',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
2,
|
||||
{
|
||||
vars: 'all',
|
||||
args: 'after-used',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-use-before-define': [
|
||||
2,
|
||||
{
|
||||
classes: false,
|
||||
functions: false,
|
||||
variables: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-var-requires': 0,
|
||||
'array-callback-return': 2,
|
||||
'arrow-body-style': 0,
|
||||
'block-scoped-var': 2,
|
||||
'capitalized-comments': 0,
|
||||
'class-methods-use-this': 0,
|
||||
complexity: [
|
||||
0,
|
||||
10,
|
||||
],
|
||||
'consistent-return': 2,
|
||||
'consistent-this': [
|
||||
2,
|
||||
'self',
|
||||
],
|
||||
'constructor-super': 2,
|
||||
curly: [
|
||||
2,
|
||||
'all',
|
||||
],
|
||||
'dot-notation': 2,
|
||||
eqeqeq: [
|
||||
2,
|
||||
'smart',
|
||||
],
|
||||
'func-name-matching': 0,
|
||||
'func-names': 2,
|
||||
'func-style': [
|
||||
2,
|
||||
'declaration',
|
||||
{
|
||||
allowArrowFunctions: true,
|
||||
},
|
||||
],
|
||||
'global-require': 2,
|
||||
'guard-for-in': 2,
|
||||
'headers/header-format': [
|
||||
'error',
|
||||
{
|
||||
source: 'string',
|
||||
style: 'line',
|
||||
content: 'Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.\nSee LICENSE.txt for license information.',
|
||||
trailingNewlines: 2,
|
||||
},
|
||||
],
|
||||
'id-blacklist': 0,
|
||||
'import/no-duplicates': 2,
|
||||
'import/no-unresolved': 0, // Handled better by TS
|
||||
'import/order': [
|
||||
2,
|
||||
{
|
||||
'newlines-between': 'always',
|
||||
groups: [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
'sibling',
|
||||
'parent',
|
||||
'index',
|
||||
],
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: '@mattermost/**',
|
||||
group: 'external',
|
||||
position: 'after',
|
||||
},
|
||||
{
|
||||
pattern: 'mattermost-redux/**',
|
||||
group: 'external',
|
||||
position: 'after',
|
||||
},
|
||||
{
|
||||
pattern: '@(selectors|actions|stores|store|reducers){,/**}',
|
||||
group: 'external',
|
||||
position: 'after',
|
||||
},
|
||||
{
|
||||
pattern: 'components/**',
|
||||
group: 'external',
|
||||
position: 'after',
|
||||
},
|
||||
{
|
||||
pattern: 'types{,/**}',
|
||||
group: 'internal',
|
||||
position: 'after',
|
||||
},
|
||||
],
|
||||
alphabetize: {
|
||||
order: 'asc',
|
||||
caseInsensitive: true,
|
||||
},
|
||||
distinctGroup: true,
|
||||
pathGroupsExcludedImportTypes: ['builtin'],
|
||||
},
|
||||
],
|
||||
'line-comment-position': 0,
|
||||
'max-lines': [
|
||||
'warn',
|
||||
{
|
||||
max: 800,
|
||||
skipBlankLines: true,
|
||||
skipComments: false,
|
||||
},
|
||||
],
|
||||
'max-nested-callbacks': ['error', 10],
|
||||
'new-cap': 2,
|
||||
'newline-before-return': 0,
|
||||
'newline-per-chained-call': 0,
|
||||
'no-alert': 2,
|
||||
'no-array-constructor': 2,
|
||||
'no-await-in-loop': 2,
|
||||
'no-caller': 2,
|
||||
'no-case-declarations': 2,
|
||||
'no-class-assign': 2,
|
||||
'no-compare-neg-zero': 2,
|
||||
'no-cond-assign': [
|
||||
2,
|
||||
'except-parens',
|
||||
],
|
||||
'no-console': 2,
|
||||
'no-const-assign': 2,
|
||||
'no-constant-binary-expression': 2,
|
||||
'no-constant-condition': 2,
|
||||
'no-debugger': 2,
|
||||
'no-div-regex': 2,
|
||||
'no-dupe-args': 2,
|
||||
'no-dupe-class-members': 0, // Handled by @typescript-eslint/no-dupe-class-members
|
||||
'no-dupe-keys': 2,
|
||||
'no-duplicate-case': 2,
|
||||
'no-duplicate-imports': 0, // Handled by import/no-duplicates
|
||||
'no-else-return': 2,
|
||||
'no-empty': 2,
|
||||
'no-empty-function': 0,
|
||||
'no-empty-pattern': 2,
|
||||
'no-eval': 2,
|
||||
'no-ex-assign': 2,
|
||||
'no-extend-native': 2,
|
||||
'no-extra-bind': 2,
|
||||
'no-extra-label': 2,
|
||||
'no-fallthrough': 2,
|
||||
'no-func-assign': 2,
|
||||
'no-global-assign': 2,
|
||||
'no-implicit-coercion': 2,
|
||||
'no-implicit-globals': 0,
|
||||
'no-implied-eval': 2,
|
||||
'no-inner-declarations': 0,
|
||||
'no-invalid-regexp': 2,
|
||||
'no-irregular-whitespace': 2,
|
||||
'no-iterator': 2,
|
||||
'no-labels': 2,
|
||||
'no-lone-blocks': 2,
|
||||
'no-lonely-if': 2,
|
||||
'no-loop-func': 2,
|
||||
'no-magic-numbers': 0,
|
||||
'no-multi-assign': 2,
|
||||
'no-multi-str': 0,
|
||||
'no-native-reassign': 2,
|
||||
'no-negated-condition': 2,
|
||||
'no-nested-ternary': 2,
|
||||
'no-new': 2,
|
||||
'no-new-func': 2,
|
||||
'no-new-object': 2,
|
||||
'no-new-symbol': 2,
|
||||
'no-new-wrappers': 2,
|
||||
'no-octal-escape': 2,
|
||||
'no-param-reassign': 2,
|
||||
'no-process-env': 2,
|
||||
'no-process-exit': 2,
|
||||
'no-proto': 2,
|
||||
'no-prototype-builtins': 2,
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'redux',
|
||||
importNames: ['DeepPartial'],
|
||||
message: 'Use DeepPartial from @mattermost/types/utilities instead.',
|
||||
},
|
||||
{
|
||||
name: 'lodash',
|
||||
message: 'Import individual functions from lodash/<function> instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-return-assign': [
|
||||
2,
|
||||
'always',
|
||||
],
|
||||
'no-return-await': 2,
|
||||
'no-script-url': 2,
|
||||
'no-self-assign': [
|
||||
2,
|
||||
{
|
||||
props: true,
|
||||
},
|
||||
],
|
||||
'no-self-compare': 2,
|
||||
'no-sequences': 2,
|
||||
'no-shadow': 0, // This isn't currently enabled, but it probably should be
|
||||
'no-shadow-restricted-names': 2,
|
||||
'no-template-curly-in-string': 2,
|
||||
'no-ternary': 0,
|
||||
'no-this-before-super': 2,
|
||||
'no-throw-literal': 2,
|
||||
'no-undef-init': 2,
|
||||
'no-undefined': 0,
|
||||
'no-underscore-dangle': 2,
|
||||
'no-unexpected-multiline': 2,
|
||||
'no-unmodified-loop-condition': 2,
|
||||
'no-unneeded-ternary': [
|
||||
2,
|
||||
{
|
||||
defaultAssignment: false,
|
||||
},
|
||||
],
|
||||
'no-unreachable': 2,
|
||||
'no-unsafe-finally': 2,
|
||||
'no-unsafe-negation': 2,
|
||||
'no-unused-expressions': 2,
|
||||
'no-unused-vars': 0, // Handled by @typescript-eslint/no-unused-vars
|
||||
'no-use-before-define': 0, // Handled by @typescript-eslint/no-use-before-define
|
||||
'no-useless-computed-key': 2,
|
||||
'no-useless-concat': 2,
|
||||
'no-useless-constructor': 2,
|
||||
'no-useless-escape': 2,
|
||||
'no-useless-rename': 2,
|
||||
'no-useless-return': 2,
|
||||
'no-var': 0,
|
||||
'no-void': 2,
|
||||
'no-warning-comments': 1,
|
||||
'no-with': 2,
|
||||
'object-shorthand': [
|
||||
2,
|
||||
'always',
|
||||
],
|
||||
'one-var': [
|
||||
2,
|
||||
'never',
|
||||
],
|
||||
'operator-assignment': [
|
||||
2,
|
||||
'always',
|
||||
],
|
||||
'prefer-arrow-callback': 2,
|
||||
'prefer-const': 2,
|
||||
'prefer-destructuring': 0,
|
||||
'prefer-numeric-literals': 2,
|
||||
'prefer-promise-reject-errors': 2,
|
||||
'prefer-rest-params': 2,
|
||||
'prefer-spread': 2,
|
||||
'prefer-template': 0,
|
||||
radix: 2,
|
||||
'require-yield': 2,
|
||||
'sort-imports': 0,
|
||||
'sort-keys': 0,
|
||||
'symbol-description': 2,
|
||||
'valid-typeof': [
|
||||
2,
|
||||
{
|
||||
requireStringLiterals: false,
|
||||
},
|
||||
],
|
||||
'vars-on-top': 0,
|
||||
yoda: [
|
||||
2,
|
||||
'never',
|
||||
{
|
||||
exceptRange: false,
|
||||
onlyEquality: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const testOverrides = {
|
||||
files: ['**/*.test.js', '**/*.test.jsx', '**/*.test.ts', '**/*.test.tsx', 'src/tests/**'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
after: true,
|
||||
afterAll: true,
|
||||
afterEach: true,
|
||||
before: true,
|
||||
beforeAll: true,
|
||||
beforeEach: true,
|
||||
describe: true,
|
||||
expect: true,
|
||||
it: true,
|
||||
jest: true,
|
||||
test: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'func-names': 0,
|
||||
'global-require': 0,
|
||||
'no-console': 0,
|
||||
'no-import-assign': 0,
|
||||
'max-lines': 0,
|
||||
'max-nested-callbacks': 0,
|
||||
'new-cap': 0,
|
||||
'prefer-arrow-callback': 0,
|
||||
'@typescript-eslint/no-require-imports': 0,
|
||||
},
|
||||
};
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...compat.extends('plugin:@typescript-eslint/recommended'),
|
||||
stylisticPlugin.configs['disable-legacy'],
|
||||
base,
|
||||
testOverrides,
|
||||
];
|
||||
200
webapp/platform/eslint-plugin/configs/react.js
vendored
Normal file
200
webapp/platform/eslint-plugin/configs/react.js
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||
|
||||
import base from './base.js';
|
||||
|
||||
const react = {
|
||||
files: ['**/*.jsx', '**/*.tsx'],
|
||||
rules: {
|
||||
'@stylistic/jsx-closing-bracket-location': [
|
||||
2,
|
||||
{
|
||||
location: 'tag-aligned',
|
||||
},
|
||||
],
|
||||
'@stylistic/jsx-curly-spacing': [
|
||||
2,
|
||||
'never',
|
||||
],
|
||||
'@stylistic/jsx-equals-spacing': [
|
||||
2,
|
||||
'never',
|
||||
],
|
||||
'@stylistic/jsx-first-prop-new-line': [
|
||||
2,
|
||||
'multiline',
|
||||
],
|
||||
'@stylistic/jsx-indent-props': [
|
||||
2,
|
||||
4,
|
||||
],
|
||||
'@stylistic/jsx-max-props-per-line': [
|
||||
2,
|
||||
{
|
||||
maximum: 1,
|
||||
},
|
||||
],
|
||||
'@stylistic/jsx-pascal-case': 2,
|
||||
'@stylistic/jsx-tag-spacing': [
|
||||
2,
|
||||
{
|
||||
closingSlash: 'never',
|
||||
beforeSelfClosing: 'never',
|
||||
afterOpening: 'never',
|
||||
},
|
||||
],
|
||||
'@stylistic/jsx-wrap-multilines': 2,
|
||||
'jsx-a11y/alt-text': 'warn',
|
||||
'jsx-a11y/anchor-has-content': 'error',
|
||||
'jsx-a11y/anchor-is-valid': 'warn',
|
||||
'jsx-a11y/aria-activedescendant-has-tabindex': 'error',
|
||||
'jsx-a11y/aria-props': 'error',
|
||||
'jsx-a11y/aria-proptypes': 'error',
|
||||
'jsx-a11y/aria-role': 'error',
|
||||
'jsx-a11y/aria-unsupported-elements': 'error',
|
||||
'jsx-a11y/autocomplete-valid': 'error',
|
||||
'jsx-a11y/click-events-have-key-events': 'warn',
|
||||
'jsx-a11y/heading-has-content': 'error',
|
||||
'jsx-a11y/html-has-lang': 'error',
|
||||
'jsx-a11y/iframe-has-title': 'error',
|
||||
'jsx-a11y/img-redundant-alt': 'warn',
|
||||
'jsx-a11y/interactive-supports-focus': 'warn',
|
||||
'jsx-a11y/label-has-associated-control': 'error',
|
||||
'jsx-a11y/media-has-caption': 'warn',
|
||||
'jsx-a11y/mouse-events-have-key-events': 'off',
|
||||
'jsx-a11y/no-access-key': 'error',
|
||||
'jsx-a11y/no-aria-hidden-on-focusable': 'error',
|
||||
'jsx-a11y/no-autofocus': 'warn',
|
||||
'jsx-a11y/no-distracting-elements': 'error',
|
||||
'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error',
|
||||
'jsx-a11y/no-noninteractive-element-interactions': 'warn',
|
||||
'jsx-a11y/no-noninteractive-element-to-interactive-role': 'warn',
|
||||
'jsx-a11y/no-noninteractive-tabindex': 'warn',
|
||||
'jsx-a11y/no-redundant-roles': 'warn',
|
||||
'jsx-a11y/no-static-element-interactions': 'warn',
|
||||
'jsx-a11y/role-has-required-aria-props': 'warn',
|
||||
'jsx-a11y/role-supports-aria-props': 'warn',
|
||||
'jsx-a11y/scope': 'error',
|
||||
'jsx-a11y/tabindex-no-positive': 'warn',
|
||||
'react/display-name': [
|
||||
0,
|
||||
{
|
||||
ignoreTranspilerName: false,
|
||||
},
|
||||
],
|
||||
'react/forbid-component-props': 0,
|
||||
'react/forbid-elements': [
|
||||
2,
|
||||
{
|
||||
forbid: [
|
||||
'embed',
|
||||
],
|
||||
},
|
||||
],
|
||||
'react/jsx-boolean-value': [
|
||||
2,
|
||||
'always',
|
||||
],
|
||||
'react/jsx-filename-extension': [
|
||||
2,
|
||||
{
|
||||
extensions: [
|
||||
'.jsx',
|
||||
'.tsx',
|
||||
],
|
||||
},
|
||||
],
|
||||
'react/jsx-handler-names': 0,
|
||||
'react/jsx-key': 2,
|
||||
'react/jsx-no-bind': 0,
|
||||
'react/jsx-no-comment-textnodes': 2,
|
||||
'react/jsx-no-duplicate-props': [
|
||||
2,
|
||||
{
|
||||
ignoreCase: false,
|
||||
},
|
||||
],
|
||||
'react/jsx-no-literals': 2,
|
||||
'react/jsx-no-target-blank': 2,
|
||||
'react/jsx-no-undef': 2,
|
||||
'react/jsx-uses-react': 2,
|
||||
'react/jsx-uses-vars': 2,
|
||||
'react/no-array-index-key': 1,
|
||||
'react/no-children-prop': 2,
|
||||
'react/no-danger': 0,
|
||||
'react/no-danger-with-children': 2,
|
||||
'react/no-deprecated': 1,
|
||||
'react/no-did-mount-set-state': 2,
|
||||
'react/no-did-update-set-state': 2,
|
||||
'react/no-direct-mutation-state': 2,
|
||||
'react/no-find-dom-node': 1,
|
||||
'react/no-is-mounted': 2,
|
||||
'react/no-multi-comp': [
|
||||
2,
|
||||
{
|
||||
ignoreStateless: true,
|
||||
},
|
||||
],
|
||||
'react/no-render-return-value': 2,
|
||||
'react/no-set-state': 0,
|
||||
'react/no-string-refs': 2,
|
||||
'react/no-unescaped-entities': 2,
|
||||
'react/no-unknown-property': [
|
||||
2,
|
||||
{
|
||||
ignore: ['mask-type'],
|
||||
},
|
||||
],
|
||||
'react/no-unused-prop-types': [
|
||||
1,
|
||||
{
|
||||
skipShapeProps: true,
|
||||
},
|
||||
],
|
||||
'react/prefer-es6-class': 2,
|
||||
'react/prefer-stateless-function': 0,
|
||||
'react/prop-types': 2,
|
||||
'react/require-default-props': 0,
|
||||
'react/require-optimization': 1,
|
||||
'react/require-render-return': 2,
|
||||
'react/self-closing-comp': 2,
|
||||
'react/sort-comp': 0,
|
||||
'react/style-prop-object': 2,
|
||||
},
|
||||
};
|
||||
|
||||
export default [
|
||||
...base,
|
||||
jsxA11yPlugin.flatConfigs.recommended,
|
||||
reactPlugin.configs.flat.recommended,
|
||||
reactHooksPlugin.configs.flat.recommended,
|
||||
react,
|
||||
{
|
||||
|
||||
// Disable new rules that are primarily needed to enable React Compiler because we're not using that yet
|
||||
rules: {
|
||||
'react-hooks/immutability': 0,
|
||||
'react-hooks/preserve-manual-memoization': 0,
|
||||
'react-hooks/purity': 0,
|
||||
'react-hooks/refs': 0,
|
||||
'react-hooks/set-state-in-effect': 0,
|
||||
|
||||
// This rule can be enabled once https://github.com/mattermost/mattermost/pull/36575 is merged because it
|
||||
// should be done by converting getChannelIconComponent and getArchiveIconComponent to proper components
|
||||
// instead of functions that return component constructors.
|
||||
'react-hooks/static-components': 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
react: {
|
||||
pragma: 'React',
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
8
webapp/platform/eslint-plugin/eslint.config.mjs
Normal file
8
webapp/platform/eslint-plugin/eslint.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import eslintPlugin from '@mattermost/eslint-plugin';
|
||||
|
||||
export default [
|
||||
...eslintPlugin.configs.base,
|
||||
];
|
||||
14
webapp/platform/eslint-plugin/index.js
Normal file
14
webapp/platform/eslint-plugin/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import base from './configs/base.js';
|
||||
import react from './configs/react.js';
|
||||
import rules from './rules/index.js';
|
||||
|
||||
export default {
|
||||
configs: {
|
||||
base,
|
||||
react,
|
||||
},
|
||||
rules,
|
||||
};
|
||||
47
webapp/platform/eslint-plugin/package.json
Normal file
47
webapp/platform/eslint-plugin/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "@mattermost/eslint-plugin",
|
||||
"version": "2.0.0",
|
||||
"description": "ESLint configuration and custom rules used by Mattermost",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/mattermost/mattermost.git",
|
||||
"directory": "webapp/platform/eslint-plugin"
|
||||
},
|
||||
"author": "Mattermost, Inc.",
|
||||
"license": "Apache 2.0",
|
||||
"homepage": "https://github.com/mattermost/mattermost/tree/master/webapp/platform/eslint-plugin#readme",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.61.0",
|
||||
"@typescript-eslint/parser": "^8.61.0",
|
||||
"eslint-plugin-headers": "^1.3.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"jsx-ast-utils": "^3.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint-plugin-react": {
|
||||
"optional": true
|
||||
},
|
||||
"eslint-plugin-react-hooks": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"check": "eslint . --quiet",
|
||||
"fix": "eslint . --quiet --fix",
|
||||
"test": "node --test rules/*.test.js",
|
||||
"clean": "rm -rf node_modules"
|
||||
}
|
||||
}
|
||||
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