eslint-plugin-rxjs-x 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,7 +72,7 @@ The package includes the following rules.
72
72
  | [just](docs/rules/just.md) | Require the use of `just` instead of `of`. | | 🔧 | | | |
73
73
  | [macro](docs/rules/macro.md) | Require the use of the RxJS Tools Babel macro. | | 🔧 | | | ❌ |
74
74
  | [no-async-subscribe](docs/rules/no-async-subscribe.md) | Disallow passing `async` functions to `subscribe`. | ✅ | | | 💭 | |
75
- | [no-compat](docs/rules/no-compat.md) | Disallow the `rxjs-compat` package. | | | | | |
75
+ | [no-compat](docs/rules/no-compat.md) | Disallow the `rxjs-compat` package. | | | | ||
76
76
  | [no-connectable](docs/rules/no-connectable.md) | Disallow operators that return connectable observables. | | | | 💭 | |
77
77
  | [no-create](docs/rules/no-create.md) | Disallow the static `Observable.create` function. | ✅ | | | 💭 | |
78
78
  | [no-cyclic-action](docs/rules/no-cyclic-action.md) | Disallow cyclic actions in effects and epics. | | | | 💭 | |
@@ -98,7 +98,7 @@ The package includes the following rules.
98
98
  | [no-subject-value](docs/rules/no-subject-value.md) | Disallow accessing the `value` property of a `BehaviorSubject` instance. | | | | 💭 | |
99
99
  | [no-subscribe-handlers](docs/rules/no-subscribe-handlers.md) | Disallow passing handlers to `subscribe`. | | | | 💭 | |
100
100
  | [no-tap](docs/rules/no-tap.md) | Disallow the `tap` operator. | | | | | ❌ |
101
- | [no-topromise](docs/rules/no-topromise.md) | Disallow use of the `toPromise` method. | | | | 💭 | |
101
+ | [no-topromise](docs/rules/no-topromise.md) | Disallow use of the `toPromise` method. | | | 💡 | 💭 | |
102
102
  | [no-unbound-methods](docs/rules/no-unbound-methods.md) | Disallow passing unbound methods. | ✅ | | | 💭 | |
103
103
  | [no-unsafe-catch](docs/rules/no-unsafe-catch.md) | Disallow unsafe `catchError` usage in effects and epics. | | | | 💭 | |
104
104
  | [no-unsafe-first](docs/rules/no-unsafe-first.md) | Disallow unsafe `first`/`take` usage in effects and epics. | | | | 💭 | |
@@ -106,6 +106,7 @@ The package includes the following rules.
106
106
  | [no-unsafe-switchmap](docs/rules/no-unsafe-switchmap.md) | Disallow unsafe `switchMap` usage in effects and epics. | | | | 💭 | |
107
107
  | [no-unsafe-takeuntil](docs/rules/no-unsafe-takeuntil.md) | Disallow applying operators after `takeUntil`. | ✅ | | | 💭 | |
108
108
  | [prefer-observer](docs/rules/prefer-observer.md) | Disallow passing separate handlers to `subscribe` and `tap`. | | 🔧 | 💡 | 💭 | |
109
+ | [prefer-root-operators](docs/rules/prefer-root-operators.md) | Disallow importing operators from `rxjs/operators`. | | 🔧 | 💡 | | |
109
110
  | [suffix-subjects](docs/rules/suffix-subjects.md) | Enforce the use of a suffix in subject identifiers. | | | | 💭 | |
110
111
  | [throw-error](docs/rules/throw-error.md) | Enforce passing only `Error` values to `throwError`. | | | | 💭 | |
111
112
 
package/dist/index.d.mts CHANGED
@@ -87,7 +87,7 @@ declare const rxjsX: {
87
87
  'no-subject-value': TSESLint.RuleModule<"forbidden", [], RxjsXRuleDocs, TSESLint.RuleListener>;
88
88
  'no-subscribe-handlers': TSESLint.RuleModule<"forbidden", [], RxjsXRuleDocs, TSESLint.RuleListener>;
89
89
  'no-tap': TSESLint.RuleModule<"forbidden", [], RxjsXRuleDocs, TSESLint.RuleListener>;
90
- 'no-topromise': TSESLint.RuleModule<"forbidden", [], RxjsXRuleDocs, TSESLint.RuleListener>;
90
+ 'no-topromise': TSESLint.RuleModule<"forbidden" | "suggestLastValueFrom" | "suggestFirstValueFrom", [], RxjsXRuleDocs, TSESLint.RuleListener>;
91
91
  'no-unbound-methods': TSESLint.RuleModule<"forbidden", [], RxjsXRuleDocs, TSESLint.RuleListener>;
92
92
  'no-unsafe-catch': TSESLint.RuleModule<"forbidden", readonly {
93
93
  observable?: string;
@@ -108,6 +108,7 @@ declare const rxjsX: {
108
108
  'prefer-observer': TSESLint.RuleModule<"forbidden", readonly {
109
109
  allowNext?: boolean;
110
110
  }[], RxjsXRuleDocs, TSESLint.RuleListener>;
111
+ 'prefer-root-operators': TSESLint.RuleModule<"forbidden" | "suggest" | "forbiddenWithoutFix", [], RxjsXRuleDocs, TSESLint.RuleListener>;
111
112
  'suffix-subjects': TSESLint.RuleModule<"forbidden", readonly {
112
113
  parameters?: boolean;
113
114
  properties?: boolean;
package/dist/index.d.ts CHANGED
@@ -87,7 +87,7 @@ declare const rxjsX: {
87
87
  'no-subject-value': TSESLint.RuleModule<"forbidden", [], RxjsXRuleDocs, TSESLint.RuleListener>;
88
88
  'no-subscribe-handlers': TSESLint.RuleModule<"forbidden", [], RxjsXRuleDocs, TSESLint.RuleListener>;
89
89
  'no-tap': TSESLint.RuleModule<"forbidden", [], RxjsXRuleDocs, TSESLint.RuleListener>;
90
- 'no-topromise': TSESLint.RuleModule<"forbidden", [], RxjsXRuleDocs, TSESLint.RuleListener>;
90
+ 'no-topromise': TSESLint.RuleModule<"forbidden" | "suggestLastValueFrom" | "suggestFirstValueFrom", [], RxjsXRuleDocs, TSESLint.RuleListener>;
91
91
  'no-unbound-methods': TSESLint.RuleModule<"forbidden", [], RxjsXRuleDocs, TSESLint.RuleListener>;
92
92
  'no-unsafe-catch': TSESLint.RuleModule<"forbidden", readonly {
93
93
  observable?: string;
@@ -108,6 +108,7 @@ declare const rxjsX: {
108
108
  'prefer-observer': TSESLint.RuleModule<"forbidden", readonly {
109
109
  allowNext?: boolean;
110
110
  }[], RxjsXRuleDocs, TSESLint.RuleListener>;
111
+ 'prefer-root-operators': TSESLint.RuleModule<"forbidden" | "suggest" | "forbiddenWithoutFix", [], RxjsXRuleDocs, TSESLint.RuleListener>;
111
112
  'suffix-subjects': TSESLint.RuleModule<"forbidden", readonly {
112
113
  parameters?: boolean;
113
114
  properties?: boolean;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }// package.json
2
2
  var name = "eslint-plugin-rxjs-x";
3
- var version = "0.3.1";
3
+ var version = "0.3.2";
4
4
 
5
5
  // src/configs/recommended.ts
6
6
  var createRecommendedConfig = (plugin2) => ({
@@ -265,6 +265,15 @@ function isFunctionExpression(node) {
265
265
  function isIdentifier(node) {
266
266
  return node.type === _utils.AST_NODE_TYPES.Identifier;
267
267
  }
268
+ function isImportDeclaration(node) {
269
+ return node.type === _utils.AST_NODE_TYPES.ImportDeclaration;
270
+ }
271
+ function isImportNamespaceSpecifier(node) {
272
+ return node.type === _utils.AST_NODE_TYPES.ImportNamespaceSpecifier;
273
+ }
274
+ function isImportSpecifier(node) {
275
+ return node.type === _utils.AST_NODE_TYPES.ImportSpecifier;
276
+ }
268
277
  function isLiteral(node) {
269
278
  return node.type === _utils.AST_NODE_TYPES.Literal;
270
279
  }
@@ -814,6 +823,7 @@ var noAsyncSubscribeRule = ruleCreator({
814
823
  var noCompatRule = ruleCreator({
815
824
  defaultOptions: [],
816
825
  meta: {
826
+ deprecated: true,
817
827
  docs: {
818
828
  description: "Disallow the `rxjs-compat` package."
819
829
  },
@@ -2327,8 +2337,11 @@ var noTopromiseRule = ruleCreator({
2327
2337
  description: "Disallow use of the `toPromise` method.",
2328
2338
  requiresTypeChecking: true
2329
2339
  },
2340
+ hasSuggestions: true,
2330
2341
  messages: {
2331
- forbidden: "The toPromise method is forbidden."
2342
+ forbidden: "The toPromise method is forbidden.",
2343
+ suggestLastValueFrom: "Use lastValueFrom instead.",
2344
+ suggestFirstValueFrom: "Use firstValueFrom instead."
2332
2345
  },
2333
2346
  schema: [],
2334
2347
  type: "problem"
@@ -2336,14 +2349,74 @@ var noTopromiseRule = ruleCreator({
2336
2349
  name: "no-topromise",
2337
2350
  create: (context) => {
2338
2351
  const { couldBeObservable } = getTypeServices(context);
2352
+ function getQuote(raw) {
2353
+ const match = /^\s*('|")/.exec(raw);
2354
+ if (!match) {
2355
+ return void 0;
2356
+ }
2357
+ const [, quote] = match;
2358
+ return quote;
2359
+ }
2360
+ function createFix(conversion, callExpression, observableNode, importDeclarations) {
2361
+ return function* fix(fixer) {
2362
+ var _a, _b, _c;
2363
+ let namespace = "";
2364
+ let functionName = conversion;
2365
+ const rxjsImportDeclaration = importDeclarations.find((node) => node.source.value === "rxjs");
2366
+ if ((_a = rxjsImportDeclaration == null ? void 0 : rxjsImportDeclaration.specifiers) == null ? void 0 : _a.every(isImportNamespaceSpecifier)) {
2367
+ namespace = rxjsImportDeclaration.specifiers[0].local.name + ".";
2368
+ } else if ((_b = rxjsImportDeclaration == null ? void 0 : rxjsImportDeclaration.specifiers) == null ? void 0 : _b.every(isImportSpecifier)) {
2369
+ const { specifiers } = rxjsImportDeclaration;
2370
+ const existingSpecifier = specifiers.find((node) => (isIdentifier(node.imported) ? node.imported.name : node.imported.value) === functionName);
2371
+ if (existingSpecifier) {
2372
+ functionName = existingSpecifier.local.name;
2373
+ } else {
2374
+ const lastSpecifier = specifiers[specifiers.length - 1];
2375
+ yield fixer.insertTextAfter(lastSpecifier, `, ${functionName}`);
2376
+ }
2377
+ } else if (importDeclarations.length) {
2378
+ const lastImport = importDeclarations[importDeclarations.length - 1];
2379
+ const quote = (_c = getQuote(lastImport.source.raw)) != null ? _c : '"';
2380
+ yield fixer.insertTextAfter(
2381
+ importDeclarations[importDeclarations.length - 1],
2382
+ `
2383
+ import { ${functionName} } from ${quote}rxjs${quote};`
2384
+ );
2385
+ } else {
2386
+ console.warn("No import declarations found. Unable to suggest a fix.");
2387
+ return;
2388
+ }
2389
+ yield fixer.replaceText(
2390
+ callExpression,
2391
+ `${namespace}${functionName}(${context.sourceCode.getText(observableNode)})`
2392
+ );
2393
+ };
2394
+ }
2339
2395
  return {
2340
- [`MemberExpression[property.name="toPromise"]`]: (node) => {
2341
- if (couldBeObservable(node.object)) {
2342
- context.report({
2343
- messageId: "forbidden",
2344
- node: node.property
2345
- });
2396
+ [`CallExpression[callee.property.name="toPromise"]`]: (node) => {
2397
+ const memberExpression = node.callee;
2398
+ if (!couldBeObservable(memberExpression.object)) {
2399
+ return;
2400
+ }
2401
+ const { body } = context.sourceCode.ast;
2402
+ const importDeclarations = body.filter(isImportDeclaration);
2403
+ if (!importDeclarations.length) {
2404
+ return;
2346
2405
  }
2406
+ context.report({
2407
+ messageId: "forbidden",
2408
+ node: memberExpression.property,
2409
+ suggest: [
2410
+ {
2411
+ messageId: "suggestLastValueFrom",
2412
+ fix: createFix("lastValueFrom", node, memberExpression.object, importDeclarations)
2413
+ },
2414
+ {
2415
+ messageId: "suggestFirstValueFrom",
2416
+ fix: createFix("firstValueFrom", node, memberExpression.object, importDeclarations)
2417
+ }
2418
+ ]
2419
+ });
2347
2420
  }
2348
2421
  };
2349
2422
  }
@@ -2927,6 +3000,142 @@ function isValidArgText(argText) {
2927
3000
  return argText && argText !== "undefined" && argText !== "null";
2928
3001
  }
2929
3002
 
3003
+ // src/rules/prefer-root-operators.ts
3004
+ var RENAMED_OPERATORS = {
3005
+ combineLatest: "combineLatestWith",
3006
+ concat: "concatWith",
3007
+ merge: "mergeWith",
3008
+ onErrorResumeNext: "onErrorResumeNextWith",
3009
+ race: "raceWith",
3010
+ zip: "zipWith"
3011
+ };
3012
+ var DEPRECATED_OPERATORS = [
3013
+ "partition"
3014
+ ];
3015
+ var preferRootOperatorsRule = ruleCreator({
3016
+ defaultOptions: [],
3017
+ meta: {
3018
+ docs: {
3019
+ description: "Disallow importing operators from `rxjs/operators`."
3020
+ },
3021
+ fixable: "code",
3022
+ hasSuggestions: true,
3023
+ messages: {
3024
+ forbidden: "RxJS imports from `rxjs/operators` are forbidden; import from `rxjs` instead.",
3025
+ forbiddenWithoutFix: "RxJS imports from `rxjs/operators` are forbidden; import from `rxjs` instead. Note some operators may have been renamed or deprecated.",
3026
+ suggest: "Replace with import from `rxjs`."
3027
+ },
3028
+ schema: [],
3029
+ type: "suggestion"
3030
+ },
3031
+ name: "prefer-root-operators",
3032
+ create: (context) => {
3033
+ function getQuote(raw) {
3034
+ const match = /^\s*('|")/.exec(raw);
3035
+ if (!match) {
3036
+ return void 0;
3037
+ }
3038
+ const [, quote] = match;
3039
+ return quote;
3040
+ }
3041
+ function getSourceReplacement(rawLocation) {
3042
+ const quote = getQuote(rawLocation);
3043
+ if (!quote) {
3044
+ return void 0;
3045
+ }
3046
+ if (/^['"]rxjs\/operators/.test(rawLocation)) {
3047
+ return `${quote}rxjs${quote}`;
3048
+ }
3049
+ return void 0;
3050
+ }
3051
+ function hasDeprecatedOperators(specifiers) {
3052
+ return !!(specifiers == null ? void 0 : specifiers.some((s) => DEPRECATED_OPERATORS.includes(getName(getOperatorNode(s)))));
3053
+ }
3054
+ function getName(node) {
3055
+ return isIdentifier(node) ? node.name : node.value;
3056
+ }
3057
+ function getOperatorNode(node) {
3058
+ return isImportSpecifier(node) ? node.imported : node.local;
3059
+ }
3060
+ function getAliasNode(node) {
3061
+ return isImportSpecifier(node) ? node.local : node.exported;
3062
+ }
3063
+ function getOperatorReplacement(name2) {
3064
+ return RENAMED_OPERATORS[name2];
3065
+ }
3066
+ function isNodesEqual(a, b) {
3067
+ return a.range[0] === b.range[0] && a.range[1] === b.range[1];
3068
+ }
3069
+ function createFix(source, replacement, specifiers) {
3070
+ return function* fix(fixer) {
3071
+ yield fixer.replaceText(source, replacement);
3072
+ for (const specifier of specifiers) {
3073
+ const operatorNode = getOperatorNode(specifier);
3074
+ const operatorName = getName(operatorNode);
3075
+ const operatorReplacement = getOperatorReplacement(operatorName);
3076
+ if (!operatorReplacement) {
3077
+ continue;
3078
+ }
3079
+ const aliasNode = getAliasNode(specifier);
3080
+ if (isNodesEqual(aliasNode, operatorNode)) {
3081
+ yield fixer.insertTextBefore(operatorNode, operatorReplacement + " as ");
3082
+ } else if (isIdentifier(operatorNode)) {
3083
+ yield fixer.replaceText(operatorNode, operatorReplacement);
3084
+ } else {
3085
+ const quote = getQuote(operatorNode.raw);
3086
+ if (!quote) {
3087
+ continue;
3088
+ }
3089
+ yield fixer.replaceText(operatorNode, quote + operatorReplacement + quote);
3090
+ }
3091
+ }
3092
+ };
3093
+ }
3094
+ function reportNode(source, specifiers) {
3095
+ const replacement = getSourceReplacement(source.raw);
3096
+ if (!replacement || hasDeprecatedOperators(specifiers)) {
3097
+ context.report({
3098
+ messageId: "forbiddenWithoutFix",
3099
+ node: source
3100
+ });
3101
+ return;
3102
+ }
3103
+ if (!specifiers) {
3104
+ context.report({
3105
+ messageId: "forbiddenWithoutFix",
3106
+ node: source,
3107
+ suggest: [{ messageId: "suggest", fix: (fixer) => fixer.replaceText(source, replacement) }]
3108
+ });
3109
+ return;
3110
+ }
3111
+ const fix = createFix(source, replacement, specifiers);
3112
+ context.report({
3113
+ fix,
3114
+ messageId: "forbidden",
3115
+ node: source,
3116
+ suggest: [{ messageId: "suggest", fix }]
3117
+ });
3118
+ }
3119
+ return {
3120
+ 'ImportDeclaration[source.value="rxjs/operators"]': (node) => {
3121
+ const specifiers = node.specifiers.length && node.specifiers.every((importClause) => isImportSpecifier(importClause)) ? node.specifiers : void 0;
3122
+ reportNode(node.source, specifiers);
3123
+ },
3124
+ 'ImportExpression[source.value="rxjs/operators"]': (node) => {
3125
+ if (isLiteral(node.source)) {
3126
+ reportNode(node.source);
3127
+ }
3128
+ },
3129
+ 'ExportNamedDeclaration[source.value="rxjs/operators"]': (node) => {
3130
+ reportNode(node.source, node.specifiers);
3131
+ },
3132
+ 'ExportAllDeclaration[source.value="rxjs/operators"]': (node) => {
3133
+ reportNode(node.source);
3134
+ }
3135
+ };
3136
+ }
3137
+ });
3138
+
2930
3139
  // src/rules/suffix-subjects.ts
2931
3140
 
2932
3141
  var defaultOptions13 = [];
@@ -3244,6 +3453,7 @@ var plugin = {
3244
3453
  "no-unsafe-switchmap": noUnsafeSwitchmapRule,
3245
3454
  "no-unsafe-takeuntil": noUnsafeTakeuntilRule,
3246
3455
  "prefer-observer": preferObserverRule,
3456
+ "prefer-root-operators": preferRootOperatorsRule,
3247
3457
  "suffix-subjects": suffixSubjectsRule,
3248
3458
  "throw-error": throwErrorRule
3249
3459
  }
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // package.json
2
2
  var name = "eslint-plugin-rxjs-x";
3
- var version = "0.3.1";
3
+ var version = "0.3.2";
4
4
 
5
5
  // src/configs/recommended.ts
6
6
  var createRecommendedConfig = (plugin2) => ({
@@ -265,6 +265,15 @@ function isFunctionExpression(node) {
265
265
  function isIdentifier(node) {
266
266
  return node.type === AST_NODE_TYPES2.Identifier;
267
267
  }
268
+ function isImportDeclaration(node) {
269
+ return node.type === AST_NODE_TYPES2.ImportDeclaration;
270
+ }
271
+ function isImportNamespaceSpecifier(node) {
272
+ return node.type === AST_NODE_TYPES2.ImportNamespaceSpecifier;
273
+ }
274
+ function isImportSpecifier(node) {
275
+ return node.type === AST_NODE_TYPES2.ImportSpecifier;
276
+ }
268
277
  function isLiteral(node) {
269
278
  return node.type === AST_NODE_TYPES2.Literal;
270
279
  }
@@ -814,6 +823,7 @@ var noAsyncSubscribeRule = ruleCreator({
814
823
  var noCompatRule = ruleCreator({
815
824
  defaultOptions: [],
816
825
  meta: {
826
+ deprecated: true,
817
827
  docs: {
818
828
  description: "Disallow the `rxjs-compat` package."
819
829
  },
@@ -2327,8 +2337,11 @@ var noTopromiseRule = ruleCreator({
2327
2337
  description: "Disallow use of the `toPromise` method.",
2328
2338
  requiresTypeChecking: true
2329
2339
  },
2340
+ hasSuggestions: true,
2330
2341
  messages: {
2331
- forbidden: "The toPromise method is forbidden."
2342
+ forbidden: "The toPromise method is forbidden.",
2343
+ suggestLastValueFrom: "Use lastValueFrom instead.",
2344
+ suggestFirstValueFrom: "Use firstValueFrom instead."
2332
2345
  },
2333
2346
  schema: [],
2334
2347
  type: "problem"
@@ -2336,14 +2349,74 @@ var noTopromiseRule = ruleCreator({
2336
2349
  name: "no-topromise",
2337
2350
  create: (context) => {
2338
2351
  const { couldBeObservable } = getTypeServices(context);
2352
+ function getQuote(raw) {
2353
+ const match = /^\s*('|")/.exec(raw);
2354
+ if (!match) {
2355
+ return void 0;
2356
+ }
2357
+ const [, quote] = match;
2358
+ return quote;
2359
+ }
2360
+ function createFix(conversion, callExpression, observableNode, importDeclarations) {
2361
+ return function* fix(fixer) {
2362
+ var _a, _b, _c;
2363
+ let namespace = "";
2364
+ let functionName = conversion;
2365
+ const rxjsImportDeclaration = importDeclarations.find((node) => node.source.value === "rxjs");
2366
+ if ((_a = rxjsImportDeclaration == null ? void 0 : rxjsImportDeclaration.specifiers) == null ? void 0 : _a.every(isImportNamespaceSpecifier)) {
2367
+ namespace = rxjsImportDeclaration.specifiers[0].local.name + ".";
2368
+ } else if ((_b = rxjsImportDeclaration == null ? void 0 : rxjsImportDeclaration.specifiers) == null ? void 0 : _b.every(isImportSpecifier)) {
2369
+ const { specifiers } = rxjsImportDeclaration;
2370
+ const existingSpecifier = specifiers.find((node) => (isIdentifier(node.imported) ? node.imported.name : node.imported.value) === functionName);
2371
+ if (existingSpecifier) {
2372
+ functionName = existingSpecifier.local.name;
2373
+ } else {
2374
+ const lastSpecifier = specifiers[specifiers.length - 1];
2375
+ yield fixer.insertTextAfter(lastSpecifier, `, ${functionName}`);
2376
+ }
2377
+ } else if (importDeclarations.length) {
2378
+ const lastImport = importDeclarations[importDeclarations.length - 1];
2379
+ const quote = (_c = getQuote(lastImport.source.raw)) != null ? _c : '"';
2380
+ yield fixer.insertTextAfter(
2381
+ importDeclarations[importDeclarations.length - 1],
2382
+ `
2383
+ import { ${functionName} } from ${quote}rxjs${quote};`
2384
+ );
2385
+ } else {
2386
+ console.warn("No import declarations found. Unable to suggest a fix.");
2387
+ return;
2388
+ }
2389
+ yield fixer.replaceText(
2390
+ callExpression,
2391
+ `${namespace}${functionName}(${context.sourceCode.getText(observableNode)})`
2392
+ );
2393
+ };
2394
+ }
2339
2395
  return {
2340
- [`MemberExpression[property.name="toPromise"]`]: (node) => {
2341
- if (couldBeObservable(node.object)) {
2342
- context.report({
2343
- messageId: "forbidden",
2344
- node: node.property
2345
- });
2396
+ [`CallExpression[callee.property.name="toPromise"]`]: (node) => {
2397
+ const memberExpression = node.callee;
2398
+ if (!couldBeObservable(memberExpression.object)) {
2399
+ return;
2400
+ }
2401
+ const { body } = context.sourceCode.ast;
2402
+ const importDeclarations = body.filter(isImportDeclaration);
2403
+ if (!importDeclarations.length) {
2404
+ return;
2346
2405
  }
2406
+ context.report({
2407
+ messageId: "forbidden",
2408
+ node: memberExpression.property,
2409
+ suggest: [
2410
+ {
2411
+ messageId: "suggestLastValueFrom",
2412
+ fix: createFix("lastValueFrom", node, memberExpression.object, importDeclarations)
2413
+ },
2414
+ {
2415
+ messageId: "suggestFirstValueFrom",
2416
+ fix: createFix("firstValueFrom", node, memberExpression.object, importDeclarations)
2417
+ }
2418
+ ]
2419
+ });
2347
2420
  }
2348
2421
  };
2349
2422
  }
@@ -2927,6 +3000,142 @@ function isValidArgText(argText) {
2927
3000
  return argText && argText !== "undefined" && argText !== "null";
2928
3001
  }
2929
3002
 
3003
+ // src/rules/prefer-root-operators.ts
3004
+ var RENAMED_OPERATORS = {
3005
+ combineLatest: "combineLatestWith",
3006
+ concat: "concatWith",
3007
+ merge: "mergeWith",
3008
+ onErrorResumeNext: "onErrorResumeNextWith",
3009
+ race: "raceWith",
3010
+ zip: "zipWith"
3011
+ };
3012
+ var DEPRECATED_OPERATORS = [
3013
+ "partition"
3014
+ ];
3015
+ var preferRootOperatorsRule = ruleCreator({
3016
+ defaultOptions: [],
3017
+ meta: {
3018
+ docs: {
3019
+ description: "Disallow importing operators from `rxjs/operators`."
3020
+ },
3021
+ fixable: "code",
3022
+ hasSuggestions: true,
3023
+ messages: {
3024
+ forbidden: "RxJS imports from `rxjs/operators` are forbidden; import from `rxjs` instead.",
3025
+ forbiddenWithoutFix: "RxJS imports from `rxjs/operators` are forbidden; import from `rxjs` instead. Note some operators may have been renamed or deprecated.",
3026
+ suggest: "Replace with import from `rxjs`."
3027
+ },
3028
+ schema: [],
3029
+ type: "suggestion"
3030
+ },
3031
+ name: "prefer-root-operators",
3032
+ create: (context) => {
3033
+ function getQuote(raw) {
3034
+ const match = /^\s*('|")/.exec(raw);
3035
+ if (!match) {
3036
+ return void 0;
3037
+ }
3038
+ const [, quote] = match;
3039
+ return quote;
3040
+ }
3041
+ function getSourceReplacement(rawLocation) {
3042
+ const quote = getQuote(rawLocation);
3043
+ if (!quote) {
3044
+ return void 0;
3045
+ }
3046
+ if (/^['"]rxjs\/operators/.test(rawLocation)) {
3047
+ return `${quote}rxjs${quote}`;
3048
+ }
3049
+ return void 0;
3050
+ }
3051
+ function hasDeprecatedOperators(specifiers) {
3052
+ return !!(specifiers == null ? void 0 : specifiers.some((s) => DEPRECATED_OPERATORS.includes(getName(getOperatorNode(s)))));
3053
+ }
3054
+ function getName(node) {
3055
+ return isIdentifier(node) ? node.name : node.value;
3056
+ }
3057
+ function getOperatorNode(node) {
3058
+ return isImportSpecifier(node) ? node.imported : node.local;
3059
+ }
3060
+ function getAliasNode(node) {
3061
+ return isImportSpecifier(node) ? node.local : node.exported;
3062
+ }
3063
+ function getOperatorReplacement(name2) {
3064
+ return RENAMED_OPERATORS[name2];
3065
+ }
3066
+ function isNodesEqual(a, b) {
3067
+ return a.range[0] === b.range[0] && a.range[1] === b.range[1];
3068
+ }
3069
+ function createFix(source, replacement, specifiers) {
3070
+ return function* fix(fixer) {
3071
+ yield fixer.replaceText(source, replacement);
3072
+ for (const specifier of specifiers) {
3073
+ const operatorNode = getOperatorNode(specifier);
3074
+ const operatorName = getName(operatorNode);
3075
+ const operatorReplacement = getOperatorReplacement(operatorName);
3076
+ if (!operatorReplacement) {
3077
+ continue;
3078
+ }
3079
+ const aliasNode = getAliasNode(specifier);
3080
+ if (isNodesEqual(aliasNode, operatorNode)) {
3081
+ yield fixer.insertTextBefore(operatorNode, operatorReplacement + " as ");
3082
+ } else if (isIdentifier(operatorNode)) {
3083
+ yield fixer.replaceText(operatorNode, operatorReplacement);
3084
+ } else {
3085
+ const quote = getQuote(operatorNode.raw);
3086
+ if (!quote) {
3087
+ continue;
3088
+ }
3089
+ yield fixer.replaceText(operatorNode, quote + operatorReplacement + quote);
3090
+ }
3091
+ }
3092
+ };
3093
+ }
3094
+ function reportNode(source, specifiers) {
3095
+ const replacement = getSourceReplacement(source.raw);
3096
+ if (!replacement || hasDeprecatedOperators(specifiers)) {
3097
+ context.report({
3098
+ messageId: "forbiddenWithoutFix",
3099
+ node: source
3100
+ });
3101
+ return;
3102
+ }
3103
+ if (!specifiers) {
3104
+ context.report({
3105
+ messageId: "forbiddenWithoutFix",
3106
+ node: source,
3107
+ suggest: [{ messageId: "suggest", fix: (fixer) => fixer.replaceText(source, replacement) }]
3108
+ });
3109
+ return;
3110
+ }
3111
+ const fix = createFix(source, replacement, specifiers);
3112
+ context.report({
3113
+ fix,
3114
+ messageId: "forbidden",
3115
+ node: source,
3116
+ suggest: [{ messageId: "suggest", fix }]
3117
+ });
3118
+ }
3119
+ return {
3120
+ 'ImportDeclaration[source.value="rxjs/operators"]': (node) => {
3121
+ const specifiers = node.specifiers.length && node.specifiers.every((importClause) => isImportSpecifier(importClause)) ? node.specifiers : void 0;
3122
+ reportNode(node.source, specifiers);
3123
+ },
3124
+ 'ImportExpression[source.value="rxjs/operators"]': (node) => {
3125
+ if (isLiteral(node.source)) {
3126
+ reportNode(node.source);
3127
+ }
3128
+ },
3129
+ 'ExportNamedDeclaration[source.value="rxjs/operators"]': (node) => {
3130
+ reportNode(node.source, node.specifiers);
3131
+ },
3132
+ 'ExportAllDeclaration[source.value="rxjs/operators"]': (node) => {
3133
+ reportNode(node.source);
3134
+ }
3135
+ };
3136
+ }
3137
+ });
3138
+
2930
3139
  // src/rules/suffix-subjects.ts
2931
3140
  import { AST_NODE_TYPES as AST_NODE_TYPES7, ESLintUtils as ESLintUtils10 } from "@typescript-eslint/utils";
2932
3141
  var defaultOptions13 = [];
@@ -3244,6 +3453,7 @@ var plugin = {
3244
3453
  "no-unsafe-switchmap": noUnsafeSwitchmapRule,
3245
3454
  "no-unsafe-takeuntil": noUnsafeTakeuntilRule,
3246
3455
  "prefer-observer": preferObserverRule,
3456
+ "prefer-root-operators": preferRootOperatorsRule,
3247
3457
  "suffix-subjects": suffixSubjectsRule,
3248
3458
  "throw-error": throwErrorRule
3249
3459
  }
@@ -1,9 +1,13 @@
1
1
  # Disallow the `rxjs-compat` package (`rxjs-x/no-compat`)
2
2
 
3
+ ❌ This rule is deprecated.
4
+
3
5
  <!-- end auto-generated rule header -->
4
6
 
5
7
  This rule prevents importing from locations that depend upon the `rxjs-compat` package.
6
8
 
9
+ This rule is deprecated because RxJS v7+ no longer includes `rxjs-compat`.
10
+
7
11
  ## Further reading
8
12
 
9
13
  - [Backwards compatibility](https://github.com/ReactiveX/rxjs/blob/a6590e971969c736a15b77154dabbc22275aa0d5/docs_app/content/guide/v6/migration.md#backwards-compatibility)
@@ -1,7 +1,13 @@
1
1
  # Disallow use of the `toPromise` method (`rxjs-x/no-topromise`)
2
2
 
3
+ 💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
4
+
3
5
  💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting).
4
6
 
5
7
  <!-- end auto-generated rule header -->
6
8
 
7
9
  This rule effects failures if the `toPromise` method is used.
10
+
11
+ ## Further reading
12
+
13
+ - [Conversion to Promises](https://rxjs.dev/deprecations/to-promise)
@@ -0,0 +1,32 @@
1
+ # Disallow importing operators from `rxjs/operators` (`rxjs-x/prefer-root-operators`)
2
+
3
+ 🔧💡 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) and manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
4
+
5
+ <!-- end auto-generated rule header -->
6
+
7
+ This rule prevents importing from the `rxjs/operators` export site.
8
+ Most operators were moved to the `rxjs` export site in RxJS v7.2.0
9
+ (excepting a few old and deprecated operators).
10
+ The `rxjs/operators` export site has since been deprecated and will be removed in a future major version.
11
+
12
+ Note that because a few operators were renamed or not migrated to the `rxjs` export site,
13
+ this rule may not provide an automatic fixer if renaming the import path is not guaranteed to be safe.
14
+ See the documentation linked below.
15
+
16
+ ## Rule details
17
+
18
+ Examples of **incorrect** code for this rule:
19
+
20
+ ```ts
21
+ import { map } from 'rxjs/operators';
22
+ ```
23
+
24
+ Examples of **correct** code for this rule:
25
+
26
+ ```ts
27
+ import { map } from 'rxjs';
28
+ ```
29
+
30
+ ## Further reading
31
+
32
+ - [Importing instructions](https://rxjs.dev/guide/importing)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eslint-plugin-rxjs-x",
3
3
  "type": "commonjs",
4
- "version": "0.3.1",
4
+ "version": "0.3.2",
5
5
  "packageManager": "yarn@4.5.1+sha512.341db9396b6e289fecc30cd7ab3af65060e05ebff4b3b47547b278b9e67b08f485ecd8c79006b405446262142c7a38154445ef7f17c1d5d1de7d90bf9ce7054d",
6
6
  "description": "ESLint v9+ plugin for RxJS",
7
7
  "author": "Jason Weinzierl <weinzierljason@gmail.com>",
@@ -84,9 +84,9 @@
84
84
  "eslint-config-flat-gitignore": "^0.3.0",
85
85
  "eslint-doc-generator": "^1.7.1",
86
86
  "eslint-import-resolver-typescript": "^3.6.3",
87
- "eslint-plugin-eslint-plugin": "^6.3.1",
87
+ "eslint-plugin-eslint-plugin": "^6.3.2",
88
88
  "eslint-plugin-import-x": "^4.4.2",
89
- "eslint-plugin-n": "^17.13.1",
89
+ "eslint-plugin-n": "^17.13.2",
90
90
  "markdownlint-cli2": "^0.15.0",
91
91
  "rxjs": "^7.8.1",
92
92
  "tsup": "^8.3.5",