eslint-plugin-no-jquery 3.0.1 → 3.1.0

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
@@ -107,6 +107,7 @@ Where rules are included in the configs `recommended`, `slim`, `all` or `depreca
107
107
  * [`no-jquery/no-data`](docs/rules/no-data.md) `all`
108
108
  * [`no-jquery/no-deferred`](docs/rules/no-deferred.md) `all`
109
109
  * [`no-jquery/no-delegate`](docs/rules/no-delegate.md) `3.0`, `all`
110
+ * [`no-jquery/no-done-fail`](docs/rules/no-done-fail.md) `all`
110
111
  * [`no-jquery/no-each`](docs/rules/no-each.md)
111
112
  * [`no-jquery/no-each-collection`](docs/rules/no-each-collection.md) `all`
112
113
  * [`no-jquery/no-each-util`](docs/rules/no-each-util.md) `all`
@@ -120,6 +121,7 @@ Where rules are included in the configs `recommended`, `slim`, `all` or `depreca
120
121
  * [`no-jquery/no-find`](docs/rules/no-find.md)
121
122
  * [`no-jquery/no-find-collection`](docs/rules/no-find-collection.md) `all`
122
123
  * [`no-jquery/no-find-util`](docs/rules/no-find-util.md) `all`
124
+ * [`no-jquery/no-fx`](docs/rules/no-fx.md) `slim`
123
125
  * [`no-jquery/no-fx-interval`](docs/rules/no-fx-interval.md) `3.0`
124
126
  * [`no-jquery/no-global-eval`](docs/rules/no-global-eval.md) `all`
125
127
  * [`no-jquery/no-global-selector`](docs/rules/no-global-selector.md) ⚙️
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-no-jquery",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "Disallow jQuery functions with native equivalents.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "devDependencies": {
36
36
  "codecov": "^3.8.3",
37
- "eslint-config-wikimedia": "^0.28.0",
37
+ "eslint-config-wikimedia": "^0.28.2",
38
38
  "eslint-docgen": "^0.7.1",
39
39
  "eslint-plugin-eslint-plugin": "^6.1.0",
40
40
  "eslint-plugin-self": "^1.2.1",
package/src/index.js CHANGED
@@ -25,6 +25,7 @@ module.exports = {
25
25
  'no-deferred': require( './rules/no-deferred' ),
26
26
  'no-delegate': require( './rules/no-delegate' ),
27
27
  'no-die': require( './rules/no-die' ),
28
+ 'no-done-fail': require( './rules/no-done-fail' ),
28
29
  'no-each': require( './rules/no-each' ),
29
30
  'no-each-collection': require( './rules/no-each-collection' ),
30
31
  'no-each-util': require( './rules/no-each-util' ),
@@ -38,6 +39,7 @@ module.exports = {
38
39
  'no-find': require( './rules/no-find' ),
39
40
  'no-find-collection': require( './rules/no-find-collection' ),
40
41
  'no-find-util': require( './rules/no-find-util' ),
42
+ 'no-fx': require( './rules/no-fx' ),
41
43
  'no-fx-interval': require( './rules/no-fx-interval' ),
42
44
  'no-global-eval': require( './rules/no-global-eval' ),
43
45
  'no-global-selector': require( './rules/no-global-selector' ),
@@ -117,6 +119,7 @@ module.exports = {
117
119
  'no-jquery/no-animate-toggle': 'error',
118
120
  'no-jquery/no-fade': 'error',
119
121
  'no-jquery/no-slide': 'error',
122
+ 'no-jquery/no-fx': 'error',
120
123
  // Ajax
121
124
  'no-jquery/no-ajax': 'error',
122
125
  'no-jquery/no-ajax-events': 'error',
@@ -333,8 +336,10 @@ module.exports = {
333
336
  'no-jquery/no-filter': 'warn',
334
337
  'no-jquery/no-prop': 'warn',
335
338
  'no-jquery/no-sub': 'warn',
336
- 'no-jquery/no-text': 'warn'
339
+ 'no-jquery/no-text': 'warn',
337
340
 
341
+ // Other methods
342
+ 'no-jquery/no-done-fail': 'warn'
338
343
  }
339
344
  }
340
345
  }
@@ -2,13 +2,15 @@
2
2
 
3
3
  const utils = require( '../utils.js' );
4
4
 
5
+ const methods = [ 'animate', 'stop', 'finish' ];
6
+
5
7
  module.exports = {
6
8
  meta: {
7
9
  type: 'suggestion',
8
10
  docs: {
9
11
  description:
10
- 'Disallows the ' + utils.jQueryCollectionLink( 'animate' ) +
11
- ' method. Use the `allowScroll` option to allow animations which are just used for scrolling. Prefer CSS transitions.'
12
+ 'Disallows the ' + methods.map( utils.jQueryCollectionLink ).join( '/' ) +
13
+ ' methods. Use the `allowScroll` option to allow animations which are just used for scrolling. Prefer CSS transitions.'
12
14
  },
13
15
  schema: [
14
16
  {
@@ -27,12 +29,12 @@ module.exports = {
27
29
  'CallExpression:exit': ( node ) => {
28
30
  if (
29
31
  node.callee.type !== 'MemberExpression' ||
30
- node.callee.property.name !== 'animate'
32
+ !methods.includes( node.callee.property.name )
31
33
  ) {
32
34
  return;
33
35
  }
34
36
  const allowScroll = context.options[ 0 ] && context.options[ 0 ].allowScroll;
35
- if ( allowScroll ) {
37
+ if ( node.callee.property.name === 'animate' && allowScroll ) {
36
38
  const arg = node.arguments[ 0 ];
37
39
  // Check properties list has more than just scrollTop/scrollLeft
38
40
  if ( arg && arg.type === 'ObjectExpression' ) {
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  const utils = require( '../utils.js' );
4
- const methods = [ 'append', 'prepend', 'before', 'after', 'replaceWith', 'add', 'appendTo', 'prependTo' ];
4
+ // htmlStrings or jQuery collections
5
+ const htmlOrCollectionMethods = [ 'append', 'prepend', 'before', 'after', 'replaceWith' ];
6
+ // htmlStrings, selectors or jQuery collections
7
+ const htmlOrSelectorOrCollectionMethods = [ 'add', 'appendTo', 'prependTo', 'insertBefore', 'insertAfter' ];
8
+ const allMethods = htmlOrCollectionMethods.concat( htmlOrSelectorOrCollectionMethods );
5
9
 
6
10
  function alljQueryOrEmpty( context, node ) {
7
11
  if ( node.type === 'ConditionalExpression' ) {
@@ -22,7 +26,7 @@ module.exports = {
22
26
  meta: {
23
27
  type: 'suggestion',
24
28
  docs: {
25
- description: 'Disallows using ' + methods.map( utils.jQueryCollectionLink ).join( '/' ) +
29
+ description: 'Disallows using ' + allMethods.map( utils.jQueryCollectionLink ).join( '/' ) +
26
30
  ' to inject HTML, in order to prevent possible XSS bugs.'
27
31
  },
28
32
  schema: []
@@ -32,13 +36,18 @@ module.exports = {
32
36
  'CallExpression:exit': ( node ) => {
33
37
  if ( !(
34
38
  node.callee.type === 'MemberExpression' &&
35
- methods.includes( node.callee.property.name )
39
+ allMethods.includes( node.callee.property.name )
36
40
  ) ) {
37
41
  return;
38
42
  }
39
43
  if ( node.arguments.every( ( arg ) => alljQueryOrEmpty( context, arg ) ) ) {
40
44
  return;
41
45
  }
46
+ if ( htmlOrSelectorOrCollectionMethods.includes( node.callee.property.name ) ) {
47
+ if ( node.arguments.every( ( arg ) => !utils.isHtmlString( arg ) ) ) {
48
+ return;
49
+ }
50
+ }
42
51
 
43
52
  if ( utils.isjQuery( context, node.callee ) ) {
44
53
  context.report( {
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ const utils = require( '../utils.js' );
4
+
5
+ module.exports = utils.createUniversalMethodRule(
6
+ [ 'done', 'fail' ],
7
+ ( node ) => node === true ?
8
+ 'Prefer `.then`' :
9
+ `Prefer .then to .${ node.callee.property.name }`,
10
+ ( method ) => `[\`.${ method }\`](https://api.jquery.com/deferred.${ method }/)`
11
+ );
@@ -44,8 +44,9 @@ const rule = utils.createCollectionMethodRule(
44
44
  'mousemove',
45
45
  'mouseout',
46
46
  'mouseover',
47
- 'mouseup'
48
- ].concat( ajaxEvents ),
47
+ 'mouseup',
48
+ ...ajaxEvents
49
+ ],
49
50
  ( node ) => node === true ?
50
51
  'Use the `allowAjaxEvents` option to allow `ajax*` methods. Prefer `.on` or `.trigger`' :
51
52
  `Prefer .on or .trigger to .${ node.callee.property.name }`,
@@ -48,7 +48,9 @@ module.exports = {
48
48
  node,
49
49
  message: 'Prefer Object.assign or the spread operator to $.extend',
50
50
  fix: function ( fixer ) {
51
- if ( !isDeep ) {
51
+ // Only auto-fix if we are sure the first argument is an object.
52
+ // If it is undefined or null variable, then Object.assign will throw.
53
+ if ( !isDeep && node.arguments[ 0 ] && node.arguments[ 0 ].type === 'ObjectExpression' ) {
52
54
  return fixer.replaceText( node.callee, 'Object.assign' );
53
55
  }
54
56
  }
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const utils = require( '../utils.js' );
4
+
5
+ module.exports = {
6
+ meta: {
7
+ type: 'suggestion',
8
+ docs: {
9
+ description: 'Disallows `$.fx`.'
10
+ },
11
+ schema: []
12
+ },
13
+
14
+ create: ( context ) => ( {
15
+ MemberExpression: ( node ) => {
16
+ if (
17
+ !utils.isjQueryConstructor( context, node.object.name ) ||
18
+ node.property.name !== 'fx'
19
+ ) {
20
+ return;
21
+ }
22
+
23
+ context.report( {
24
+ node,
25
+ message: '$.fx is not allowed'
26
+ } );
27
+ }
28
+ } )
29
+ };
@@ -2,33 +2,11 @@
2
2
 
3
3
  const utils = require( '../utils.js' );
4
4
 
5
- // HTML regex (modified from jQuery)
6
- const rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*)$/;
7
5
  // Single tag regex (from jQuery)
8
6
  const rsingleTag = /^<([a-z][^/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;
9
7
  const rsingleTagMinimal = /^<([a-z][^/\0>:\x20\t\r\n\f]*)>$/i;
10
8
  const rsingleTagSelfClosing = /^<([a-z][^/\0>:\x20\t\r\n\f]*)\/>$/i;
11
9
 
12
- function allLiteral( node ) {
13
- if ( node.type === 'BinaryExpression' ) {
14
- return allLiteral( node.left ) && allLiteral( node.right );
15
- } else {
16
- return node.type === 'Literal';
17
- }
18
- }
19
-
20
- function joinLiterals( node ) {
21
- if ( node.type === 'BinaryExpression' ) {
22
- return joinLiterals( node.left ) + joinLiterals( node.right );
23
- }
24
- /* istanbul ignore else */
25
- if ( node.type === 'Literal' ) {
26
- return node.value;
27
- }
28
- /* istanbul ignore next */
29
- throw new Error( 'Non-literal node passed to joinLiteral' );
30
- }
31
-
32
10
  module.exports = {
33
11
  meta: {
34
12
  type: 'suggestion',
@@ -94,11 +72,10 @@ module.exports = {
94
72
  let expectedTag;
95
73
  const arg = node.arguments[ 0 ];
96
74
  if ( allowSingle ) {
97
- const value = arg && allLiteral( arg ) && joinLiterals( arg );
98
- if ( !( typeof value === 'string' && value ) || !rquickExpr.exec( value ) ) {
99
- // Empty or non-string, or non-HTML
75
+ if ( !utils.isHtmlString( arg ) ) {
100
76
  return;
101
77
  }
78
+ const value = utils.joinLiterals( arg );
102
79
  let match;
103
80
  if ( ( match = rsingleTag.exec( value ) ) ) {
104
81
  // Single tag
@@ -122,7 +99,7 @@ module.exports = {
122
99
  return;
123
100
  }
124
101
  }
125
- } else if ( !( arg && allLiteral( arg ) ) ) {
102
+ } else if ( !( arg && utils.allLiteral( arg ) ) ) {
126
103
  // Non literals passed to $.parseHTML
127
104
  return;
128
105
  }
@@ -2,19 +2,6 @@
2
2
 
3
3
  const utils = require( '../utils.js' );
4
4
 
5
- function collectLiterals( node ) {
6
- if ( node.type === 'BinaryExpression' ) {
7
- return collectLiterals( node.left ) + collectLiterals( node.right );
8
- } else if ( node.type === 'Literal' ) {
9
- return node.value;
10
- } else if ( node.type === 'Identifier' ) {
11
- // Dummy value for regex matching
12
- return 'A0';
13
- } else {
14
- return '';
15
- }
16
- }
17
-
18
5
  module.exports = {
19
6
  meta: {
20
7
  type: 'suggestion',
@@ -79,7 +66,7 @@ module.exports = {
79
66
  context.options[ 0 ].allowPositional;
80
67
  const allowOther = context.options[ 0 ] &&
81
68
  context.options[ 0 ].allowOther;
82
- const value = collectLiterals( node.arguments[ 0 ] );
69
+ const value = utils.joinLiterals( node.arguments[ 0 ] );
83
70
 
84
71
  if ( !allowPositional && forbiddenPositional.test( value ) ) {
85
72
  context.report( {
package/src/utils.js CHANGED
@@ -173,6 +173,7 @@ function isjQueryConstructor( context, name ) {
173
173
  //
174
174
  // Returns true if the function call node is attached to a jQuery element set.
175
175
  function isjQuery( context, node ) {
176
+ // eslint-disable-next-line security/detect-non-literal-regexp
176
177
  const variablePattern = new RegExp(
177
178
  ( context.settings && context.settings[ 'no-jquery' ] && context.settings[ 'no-jquery' ].variablePattern ) ||
178
179
  '^\\$.'
@@ -311,7 +312,7 @@ function createCollectionMethodRule( methods, message, options ) {
311
312
  methods = Array.isArray( methods ) ? methods : [ methods ];
312
313
 
313
314
  let description = 'Disallows the ' + methods.map( jQueryCollectionLink ).join( '/' ) + ' ' +
314
- ( methods.length > 1 ? 'methods' : 'method' ) + '.';
315
+ ( methods.length > 1 ? 'methods' : 'method' ) + '.';
315
316
 
316
317
  description += messageSuffix( message );
317
318
 
@@ -369,7 +370,7 @@ function createCollectionMethodRule( methods, message, options ) {
369
370
  }
370
371
 
371
372
  /**
372
- * Create a rule for collection property
373
+ * Create a rule for collection properties
373
374
  *
374
375
  * @param {string} property Property name
375
376
  * @param {string|Function} [message] Message to report. See createCollectionMethodRule.
@@ -421,7 +422,7 @@ function createUtilMethodRule( methods, message, options ) {
421
422
  methods = Array.isArray( methods ) ? methods : [ methods ];
422
423
 
423
424
  let description = 'Disallows the ' + methods.map( jQueryGlobalLink ).join( '/' ) + ' ' +
424
- ( methods.length > 1 ? 'utilies' : 'utility' ) + '.';
425
+ ( methods.length > 1 ? 'utilies' : 'utility' ) + '.';
425
426
 
426
427
  description += messageSuffix( message );
427
428
 
@@ -448,7 +449,7 @@ function createUtilMethodRule( methods, message, options ) {
448
449
  }
449
450
 
450
451
  /**
451
- * Create a rule for util methods
452
+ * Create a rule for util properties
452
453
  *
453
454
  * @param {string} property Property name
454
455
  * @param {string|Function} [message] Message to report. See createCollectionMethodRule.
@@ -499,7 +500,7 @@ function createCollectionOrUtilMethodRule( methods, message, options ) {
499
500
  methods = Array.isArray( methods ) ? methods : [ methods ];
500
501
 
501
502
  let description = 'Disallows the ' + methods.map( jQueryCollectionLink ).join( '/' ) + ' ' +
502
- ( methods.length > 1 ? 'methods' : 'method' );
503
+ ( methods.length > 1 ? 'methods' : 'method' );
503
504
 
504
505
  description += ' and ' + methods.map( jQueryGlobalLink ).join( '/' ) + ' ' +
505
506
  ( methods.length > 1 ? 'utilies' : 'utility' ) + '.';
@@ -526,6 +527,47 @@ function createCollectionOrUtilMethodRule( methods, message, options ) {
526
527
  } ), description, options.fixable, options.deprecated );
527
528
  }
528
529
 
530
+ /**
531
+ * Create a rule for a method on any object
532
+ *
533
+ * @param {string|string[]} methods Method or list of method names
534
+ * @param {string|Function} message Message to report. See createCollectionMethodRule.
535
+ * @param {Function} linkGenerator Function to generate a markdown link
536
+ * @param {Object} [options] Options. See createCollectionMethodRule.
537
+ * for a given function name.
538
+ * @return {Object} Rule
539
+ */
540
+ function createUniversalMethodRule( methods, message, linkGenerator, options ) {
541
+ options = options || {};
542
+
543
+ options.mode = 'util';
544
+
545
+ methods = Array.isArray( methods ) ? methods : [ methods ];
546
+
547
+ let description = 'Disallows the ' + methods.map( linkGenerator ).join( '/' ) + ' ' +
548
+ ( methods.length > 1 ? 'methods' : 'method' ) + '.';
549
+
550
+ description += messageSuffix( message );
551
+
552
+ return createRule( ( context ) => ( {
553
+ 'CallExpression:exit': ( node ) => {
554
+ if ( node.callee.type !== 'MemberExpression' ) {
555
+ return;
556
+ }
557
+ const name = node.callee.property.name;
558
+ if ( !methods.includes( name ) ) {
559
+ return;
560
+ }
561
+
562
+ context.report( {
563
+ node,
564
+ message: messageToPlainString( message, node, name, options ),
565
+ fix: options.fix && options.fix.bind( this, node, context )
566
+ } );
567
+ }
568
+ } ), description, options.fixable, options.deprecated );
569
+ }
570
+
529
571
  function eventShorthandFixer( node, context, fixer ) {
530
572
  const name = node.callee.property.name;
531
573
  if ( node.callee.parent.arguments.length ) {
@@ -541,6 +583,35 @@ function eventShorthandFixer( node, context, fixer ) {
541
583
  }
542
584
  }
543
585
 
586
+ function allLiteral( node ) {
587
+ if ( node.type === 'BinaryExpression' ) {
588
+ return allLiteral( node.left ) && allLiteral( node.right );
589
+ } else {
590
+ return node.type === 'Literal';
591
+ }
592
+ }
593
+
594
+ function joinLiterals( node ) {
595
+ if ( node.type === 'BinaryExpression' ) {
596
+ return joinLiterals( node.left ) + joinLiterals( node.right );
597
+ } else if ( node.type === 'Literal' ) {
598
+ return node.value;
599
+ } else if ( node.type === 'Identifier' ) {
600
+ // Dummy value for regex matching
601
+ return 'A0';
602
+ } else {
603
+ return '';
604
+ }
605
+ }
606
+
607
+ // HTML regex (modified from jQuery)
608
+ const rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*)$/;
609
+
610
+ function isHtmlString( arg ) {
611
+ const value = arg && allLiteral( arg ) && joinLiterals( arg );
612
+ return typeof value === 'string' && value && rquickExpr.exec( value );
613
+ }
614
+
544
615
  module.exports = {
545
616
  isjQuery,
546
617
  isjQueryConstructor,
@@ -550,7 +621,11 @@ module.exports = {
550
621
  createUtilMethodRule,
551
622
  createUtilPropertyRule,
552
623
  createCollectionOrUtilMethodRule,
624
+ createUniversalMethodRule,
553
625
  eventShorthandFixer,
554
626
  jQueryCollectionLink,
555
- jQueryGlobalLink
627
+ jQueryGlobalLink,
628
+ allLiteral,
629
+ joinLiterals,
630
+ isHtmlString
556
631
  };