diff --git a/eslint-suppressions.json b/eslint-suppressions.json new file mode 100644 index 000000000000..eddbd9cba3f6 --- /dev/null +++ b/eslint-suppressions.json @@ -0,0 +1,97 @@ +{ + "packages/eslint-plugin-internal/tests/rules/plugin-test-formatting.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 26 + } + }, + "packages/eslint-plugin/tests/rules/dot-notation.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 1 + } + }, + "packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-case-insensitive-order.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 4 + } + }, + "packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-order.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 4 + } + }, + "packages/eslint-plugin/tests/rules/naming-convention/naming-convention.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 12 + } + }, + "packages/eslint-plugin/tests/rules/no-extraneous-class.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 12 + } + }, + "packages/eslint-plugin/tests/rules/no-invalid-this.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 43 + } + }, + "packages/eslint-plugin/tests/rules/no-loop-func.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 2 + } + }, + "packages/eslint-plugin/tests/rules/no-this-alias.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 14 + } + }, + "packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 44 + } + }, + "packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 1 + } + }, + "packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 2 + } + }, + "packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 154 + } + }, + "packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 9 + } + }, + "packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 42 + } + }, + "packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 17 + } + }, + "packages/eslint-plugin/tests/rules/return-await.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 1 + } + }, + "packages/eslint-plugin/tests/rules/sort-type-constituents.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 4 + } + }, + "packages/eslint-plugin/tests/rules/unbound-method.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 3 + } + } +} diff --git a/eslint.config.mjs b/eslint.config.mjs index 114cea61a45a..671119672a0d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -416,6 +416,7 @@ export default tseslint.config( ], name: 'eslint-plugin-and-eslint-plugin-internal/test-files/rules', rules: { + '@typescript-eslint/internal/no-dynamic-tests': 'error', '@typescript-eslint/internal/plugin-test-formatting': 'error', }, }, diff --git a/package.json b/package.json index 33567c469efa..c474231075e3 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "lint-markdown-fix": "yarn lint-markdown --fix", "lint-markdown": "markdownlint \"**/*.md\" --config=.markdownlint.json --ignore-path=.markdownlintignore", "lint-stylelint": "nx lint website stylelint", + "lint-prune-suppressions": "nx run-many -t lint --projects=eslint-plugin-internal,eslint-plugin --prune-suppressions", "lint": "nx run-many -t lint", "postinstall": "tsx tools/scripts/postinstall.mts", "pre-commit": "lint-staged", @@ -77,7 +78,7 @@ "console-fail-test": "^0.5.0", "cross-fetch": "^4.0.0", "cspell": "^9.0.0", - "eslint": "^9.26.0", + "eslint": "^9.30.1", "eslint-plugin-eslint-plugin": "^6.3.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsdoc": "^50.5.0", diff --git a/packages/eslint-plugin-internal/src/rules/index.ts b/packages/eslint-plugin-internal/src/rules/index.ts index 806618f909e4..e51eafd1910f 100644 --- a/packages/eslint-plugin-internal/src/rules/index.ts +++ b/packages/eslint-plugin-internal/src/rules/index.ts @@ -2,6 +2,7 @@ import type { Linter } from '@typescript-eslint/utils/ts-eslint'; import debugNamespace from './debug-namespace'; import eqeqNullish from './eqeq-nullish'; +import noDynamicTests from './no-dynamic-tests'; import noPoorlyTypedTsProps from './no-poorly-typed-ts-props'; import noRelativePathsToInternalPackages from './no-relative-paths-to-internal-packages'; import noTypescriptDefaultImport from './no-typescript-default-import'; @@ -12,6 +13,7 @@ import preferASTTypesEnum from './prefer-ast-types-enum'; export default { 'debug-namespace': debugNamespace, 'eqeq-nullish': eqeqNullish, + 'no-dynamic-tests': noDynamicTests, 'no-poorly-typed-ts-props': noPoorlyTypedTsProps, 'no-relative-paths-to-internal-packages': noRelativePathsToInternalPackages, 'no-typescript-default-import': noTypescriptDefaultImport, diff --git a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts new file mode 100644 index 000000000000..ba9625e1b03e --- /dev/null +++ b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts @@ -0,0 +1,140 @@ +import type { InvalidTestCase } from '@typescript-eslint/rule-tester'; +import type { TSESTree } from '@typescript-eslint/utils'; + +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import { createRule } from '../util'; + +export default createRule({ + name: 'no-dynamic-tests', + meta: { + type: 'problem', + docs: { + description: 'Disallow dynamic syntax in RuleTester test arrays', + }, + messages: { + noDynamicTests: + 'Dynamic syntax is not allowed in RuleTester test arrays. Use static values only.', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + function isRuleTesterCall(node: TSESTree.Node): boolean { + return ( + node.type === AST_NODE_TYPES.CallExpression && + node.callee.type === AST_NODE_TYPES.MemberExpression && + node.callee.object.type === AST_NODE_TYPES.Identifier && + node.callee.object.name === 'ruleTester' && + node.callee.property.type === AST_NODE_TYPES.Identifier && + node.callee.property.name === 'run' + ); + } + + function reportDynamicElements(node: TSESTree.Node): void { + switch (node.type) { + case AST_NODE_TYPES.CallExpression: + case AST_NODE_TYPES.SpreadElement: + case AST_NODE_TYPES.Identifier: + case AST_NODE_TYPES.BinaryExpression: + case AST_NODE_TYPES.ConditionalExpression: + case AST_NODE_TYPES.MemberExpression: + context.report({ + node, + messageId: 'noDynamicTests', + }); + break; + case AST_NODE_TYPES.TemplateLiteral: + node.expressions.forEach(expr => { + reportDynamicElements(expr); + }); + break; + case AST_NODE_TYPES.ArrayExpression: + node.elements.forEach(element => { + if (element) { + reportDynamicElements(element); + } + }); + break; + case AST_NODE_TYPES.ObjectExpression: + node.properties.forEach(prop => { + if (prop.type === AST_NODE_TYPES.SpreadElement) { + context.report({ + node: prop, + messageId: 'noDynamicTests', + }); + } else { + // InvalidTestCase extends ValidTestCase + type TestCaseKey = keyof InvalidTestCase; + const keyToValidate: TestCaseKey[] = ['code', 'errors']; + + if ( + prop.key.type === AST_NODE_TYPES.Identifier && + keyToValidate.includes(prop.key.name as TestCaseKey) + ) { + reportDynamicElements(prop.value); + } else if ( + prop.key.type === AST_NODE_TYPES.Literal && + keyToValidate.includes(prop.key.value as TestCaseKey) + ) { + reportDynamicElements(prop.value); + } + } + }); + break; + case AST_NODE_TYPES.TaggedTemplateExpression: + if ( + !( + node.tag.type === AST_NODE_TYPES.Identifier && + node.tag.name === 'noFormat' + ) + ) { + context.report({ + node: node.tag, + messageId: 'noDynamicTests', + }); + } + break; + case AST_NODE_TYPES.Literal: + default: + break; + } + } + + return { + CallExpression(node) { + if (isRuleTesterCall(node)) { + // If valid code, arg length is always 3 but we need to avoid conflict while dev + if (node.arguments.length < 3) { + return; + } + const testObject = node.arguments[2]; + + if (testObject.type === AST_NODE_TYPES.ObjectExpression) { + for (const prop of testObject.properties) { + const isTestCases = + prop.type === AST_NODE_TYPES.Property && + prop.key.type === AST_NODE_TYPES.Identifier && + (prop.key.name === 'valid' || prop.key.name === 'invalid'); + + if (isTestCases) { + if (prop.value.type === AST_NODE_TYPES.ArrayExpression) { + prop.value.elements.forEach(element => { + if (element) { + reportDynamicElements(element); + } + }); + } else { + context.report({ + node: prop.value, + messageId: 'noDynamicTests', + }); + } + } + } + } + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts new file mode 100644 index 000000000000..c530015d0eb2 --- /dev/null +++ b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts @@ -0,0 +1,289 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/no-dynamic-tests'; + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + ecmaFeatures: {}, + ecmaVersion: 6, + sourceType: 'module', + }, + }, +}); + +ruleTester.run('no-dynamic-tests', rule, { + invalid: [ + // Function calls in test arrays + { + code: ` +ruleTester.run('test', rule, { + valid: [generateTestCases()], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + { + code: ` +ruleTester.run('test', rule, { + valid: [], + invalid: [...getInvalidCases()], +}); + `, + errors: [ + { + column: 13, + line: 4, + messageId: 'noDynamicTests', + }, + ], + }, + // Spread operator in test arrays + { + code: ` +ruleTester.run('test', rule, { + valid: [...validTestCases], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + { + code: ` +ruleTester.run('test', rule, { + valid: [...validTestCases.map(t => t.code)], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + // Simple identifiers in test arrays + { + code: ` +ruleTester.run('test', rule, { + valid: [testCase], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + // Template literals in test arrays + { + code: ` +ruleTester.run('test', rule, { + valid: [\`\${getTest()}\`], + invalid: [], +}); + `, + errors: [ + { + column: 14, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + // Binary expressions in test arrays + { + code: ` +ruleTester.run('test', rule, { + valid: ['test' + getSuffix()], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + // Conditional expressions in test arrays + { + code: ` +ruleTester.run('test', rule, { + valid: [shouldTest ? 'test1' : 'test2'], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + // Member expressions in test arrays + { + code: ` +ruleTester.run('test', rule, { + valid: [testConfig.cases], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + // Object spread + { + code: ` +ruleTester.run('test', rule, { + valid: [{ ...testConfig }], + invalid: [], +}); + `, + errors: [ + { + column: 13, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + // Tag + { + code: ` +ruleTester.run('test', rule, { + valid: [foo\`const x = 1;\`], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + // Object Value + { + code: ` +ruleTester.run('test', rule, { + valid: [{ code: foo }], + invalid: [], +}); + `, + errors: [ + { + column: 19, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + { + code: ` +ruleTester.run('test', rule, { + valid: [{ errors: [...getErrors()] }], + invalid: [], +}); + `, + errors: [ + { + column: 22, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + // assign directly + { + code: ` +ruleTester.run('test', rule, { + valid: foo, + invalid: [], +}); + `, + errors: [ + { + column: 10, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + ], + valid: [ + { + code: ` +ruleTester.run('test', rule, { + valid: ['const x = 1;'], + invalid: [], +}); + `, + }, + { + code: ` +ruleTester.run('test', rule, { + valid: ['const x = 1;', 'let y = 2;'], + invalid: [ + { + code: 'var z = 3;', + errors: [{ messageId: 'error' }], + }, + ], +}); + `, + }, + { + code: ` +ruleTester.run('test', rule, { + valid: [{ code: 'const x = 1;' }, { code: 'let y = 2;' }], + invalid: [], +}); + `, + }, + { + code: ` +ruleTester.run('test', rule, { + valid: [noFormat\`const x = 1;\`], + invalid: [], +}); + `, + }, + { + code: ` +ruleTester.run('test', rule, { + code: "import type { ValueOf } from './utils';", + filename: path.resolve( + PACKAGES_DIR, + 'ast-spec/src/expression/AssignmentExpression/spec.ts', + ), +}); + `, + }, + ], +}); diff --git a/yarn.lock b/yarn.lock index 4b24883fa03d..13ca9fe1983e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3935,6 +3935,17 @@ __metadata: languageName: node linkType: hard +"@eslint/config-array@npm:^0.21.0": + version: 0.21.0 + resolution: "@eslint/config-array@npm:0.21.0" + dependencies: + "@eslint/object-schema": ^2.1.6 + debug: ^4.3.1 + minimatch: ^3.1.2 + checksum: 84d3ae7cb755af94dc158a74389f4c560757b13f2bb908f598f927b87b70a38e8152015ea2e9557c1b4afc5130ee1356f6cad682050d67aae0468bbef98bc3a8 + languageName: node + linkType: hard + "@eslint/config-helpers@npm:^0.2.1": version: 0.2.2 resolution: "@eslint/config-helpers@npm:0.2.2" @@ -3942,6 +3953,13 @@ __metadata: languageName: node linkType: hard +"@eslint/config-helpers@npm:^0.3.0": + version: 0.3.0 + resolution: "@eslint/config-helpers@npm:0.3.0" + checksum: d4fe8242ef580806ddaa88309f4bb2d3e6be5524cc6d6197675106c6d048f766a3f9cdc2e8e33bbc97a123065792cac8314fc85ac2b3cf72610e8df59301d63a + languageName: node + linkType: hard + "@eslint/core@npm:^0.13.0": version: 0.13.0 resolution: "@eslint/core@npm:0.13.0" @@ -3951,6 +3969,24 @@ __metadata: languageName: node linkType: hard +"@eslint/core@npm:^0.14.0": + version: 0.14.0 + resolution: "@eslint/core@npm:0.14.0" + dependencies: + "@types/json-schema": ^7.0.15 + checksum: d68b8282b6f38c5145234f812f18f491d12d716240875591bd54bf5ac32858d979bdf6d38e521997a6e01f2c4223a3e66049714151da7278d0a95ff15b5d46c8 + languageName: node + linkType: hard + +"@eslint/core@npm:^0.15.1": + version: 0.15.1 + resolution: "@eslint/core@npm:0.15.1" + dependencies: + "@types/json-schema": ^7.0.15 + checksum: 9215f00466d60764453466604443a491b0ea8263c148836fef723354d6ef1d550991e931d3df2780c99cee2cab14c4f41f97d5341ab12a8443236c961bb6f664 + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^3.2.0, @eslint/eslintrc@npm:^3.3.1": version: 3.3.1 resolution: "@eslint/eslintrc@npm:3.3.1" @@ -3975,6 +4011,13 @@ __metadata: languageName: node linkType: hard +"@eslint/js@npm:9.30.1": + version: 9.30.1 + resolution: "@eslint/js@npm:9.30.1" + checksum: 596adcd4336f098121b4f3f336169dabe86ca8d34b9fb4e30c9c44ccbb10def931bdbbd92cd910776c4030a05ae614fbc89fc8d09f69f5bad2795cd7157678e8 + languageName: node + linkType: hard + "@eslint/object-schema@npm:^2.1.6": version: 2.1.6 resolution: "@eslint/object-schema@npm:2.1.6" @@ -3992,6 +4035,16 @@ __metadata: languageName: node linkType: hard +"@eslint/plugin-kit@npm:^0.3.1": + version: 0.3.3 + resolution: "@eslint/plugin-kit@npm:0.3.3" + dependencies: + "@eslint/core": ^0.15.1 + levn: ^0.4.1 + checksum: c9dc7b83ed011dce35ccc66dc53aaaa87e9fb2bd7c8a11231f7624334d82c9a53552e4b1a1cb60b74073fcc49a2661be874e503aae14cf2f6ac6b1c7faeb7080 + languageName: node + linkType: hard + "@gerrit0/mini-shiki@npm:^3.2.2": version: 3.3.0 resolution: "@gerrit0/mini-shiki@npm:3.3.0" @@ -6178,7 +6231,7 @@ __metadata: console-fail-test: ^0.5.0 cross-fetch: ^4.0.0 cspell: ^9.0.0 - eslint: ^9.26.0 + eslint: ^9.30.1 eslint-plugin-eslint-plugin: ^6.3.1 eslint-plugin-import: ^2.31.0 eslint-plugin-jsdoc: ^50.5.0 @@ -6674,6 +6727,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.15.0": + version: 8.15.0 + resolution: "acorn@npm:8.15.0" + bin: + acorn: bin/acorn + checksum: 309c6b49aedf1a2e34aaf266de06de04aab6eb097c02375c66fdeb0f64556a6a823540409914fb364d9a11bc30d79d485a2eba29af47992d3490e9886c4391c3 + languageName: node + linkType: hard + "address@npm:^1.0.1, address@npm:^1.1.2": version: 1.1.2 resolution: "address@npm:1.1.2" @@ -10250,6 +10312,16 @@ __metadata: languageName: node linkType: hard +"eslint-scope@npm:^8.4.0": + version: 8.4.0 + resolution: "eslint-scope@npm:8.4.0" + dependencies: + esrecurse: ^4.3.0 + estraverse: ^5.2.0 + checksum: cf88f42cd5e81490d549dc6d350fe01e6fe420f9d9ea34f134bb359b030e3c4ef888d36667632e448937fe52449f7181501df48c08200e3d3b0fee250d05364e + languageName: node + linkType: hard + "eslint-visitor-keys@npm:^2.1.0": version: 2.1.0 resolution: "eslint-visitor-keys@npm:2.1.0" @@ -10278,7 +10350,7 @@ __metadata: languageName: node linkType: hard -"eslint@npm:*, eslint@npm:^9.15.0, eslint@npm:^9.26.0": +"eslint@npm:*, eslint@npm:^9.15.0": version: 9.26.0 resolution: "eslint@npm:9.26.0" dependencies: @@ -10330,6 +10402,56 @@ __metadata: languageName: node linkType: hard +"eslint@npm:^9.30.1": + version: 9.30.1 + resolution: "eslint@npm:9.30.1" + dependencies: + "@eslint-community/eslint-utils": ^4.2.0 + "@eslint-community/regexpp": ^4.12.1 + "@eslint/config-array": ^0.21.0 + "@eslint/config-helpers": ^0.3.0 + "@eslint/core": ^0.14.0 + "@eslint/eslintrc": ^3.3.1 + "@eslint/js": 9.30.1 + "@eslint/plugin-kit": ^0.3.1 + "@humanfs/node": ^0.16.6 + "@humanwhocodes/module-importer": ^1.0.1 + "@humanwhocodes/retry": ^0.4.2 + "@types/estree": ^1.0.6 + "@types/json-schema": ^7.0.15 + ajv: ^6.12.4 + chalk: ^4.0.0 + cross-spawn: ^7.0.6 + debug: ^4.3.2 + escape-string-regexp: ^4.0.0 + eslint-scope: ^8.4.0 + eslint-visitor-keys: ^4.2.1 + espree: ^10.4.0 + esquery: ^1.5.0 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^8.0.0 + find-up: ^5.0.0 + glob-parent: ^6.0.2 + ignore: ^5.2.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + json-stable-stringify-without-jsonify: ^1.0.1 + lodash.merge: ^4.6.2 + minimatch: ^3.1.2 + natural-compare: ^1.4.0 + optionator: ^0.9.3 + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + bin: + eslint: bin/eslint.js + checksum: e6723b98ba19ff17cf0cacb29c3c0ea5c7c6b6fb648136b2d009e7e2da4980a2562c9523623b0faf449750e890f3b274b20bee11fa9c8f43362d235485ba2f91 + languageName: node + linkType: hard + "espree@npm:^10.0.1, espree@npm:^10.1.0, espree@npm:^10.3.0": version: 10.3.0 resolution: "espree@npm:10.3.0" @@ -10341,6 +10463,17 @@ __metadata: languageName: node linkType: hard +"espree@npm:^10.4.0": + version: 10.4.0 + resolution: "espree@npm:10.4.0" + dependencies: + acorn: ^8.15.0 + acorn-jsx: ^5.3.2 + eslint-visitor-keys: ^4.2.1 + checksum: 5f9d0d7c81c1bca4bfd29a55270067ff9d575adb8c729a5d7f779c2c7b910bfc68ccf8ec19b29844b707440fc159a83868f22c8e87bbf7cbcb225ed067df6c85 + languageName: node + linkType: hard + "esprima@npm:^4.0.0, esprima@npm:^4.0.1": version: 4.0.1 resolution: "esprima@npm:4.0.1" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy