From 89f9f2d0843ebcfcfacde3b33657674068965a9b Mon Sep 17 00:00:00 2001 From: atcastle Date: Tue, 6 Oct 2020 13:18:12 -0700 Subject: [PATCH 1/8] lint rule and tests for no-unsized-images --- .../image-component/no-unsized-images.js | 43 +++++++ .../image-component/no-unsized-images.test.js | 115 ++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 packages/eslint-plugin-next/lib/rules/image-component/no-unsized-images.js create mode 100644 test/eslint-plugin-next/image-component/no-unsized-images.test.js diff --git a/packages/eslint-plugin-next/lib/rules/image-component/no-unsized-images.js b/packages/eslint-plugin-next/lib/rules/image-component/no-unsized-images.js new file mode 100644 index 000000000000..a5ef2ef3ffc8 --- /dev/null +++ b/packages/eslint-plugin-next/lib/rules/image-component/no-unsized-images.js @@ -0,0 +1,43 @@ +module.exports = { + meta: { + messages: { + unsizedImages: + 'For layout stability, the image component should be used ' + + 'with a height and width property, even if actual image size is determined with CSS.' + + 'if you cannot provide a correct-ratio height and width, use the "unsized" attribute.' + + 'More info: https://web.dev/optimize-cls/#images-without-dimensions', + }, + }, + create(context) { + let imageComponent = null + return { + ImportDeclaration(node) { + if (node.source.value === 'next/image') { + imageComponent = node.specifiers[0].local.name + } + }, + JSXOpeningElement(node) { + if (!imageComponent) { + return + } + if (node.name.name === imageComponent) { + //Image Component rules here + if (!imageValidSize(node)) { + context.report({ + node, + messageId: 'unsizedImages', + }) + } + } + }, + } + }, +} + +function imageValidSize(node) { + let attributes = node.attributes.map((attribute) => attribute.name.name) + return ( + attributes.includes('unsized') || + (attributes.includes('height') && attributes.includes('width')) + ) +} diff --git a/test/eslint-plugin-next/image-component/no-unsized-images.test.js b/test/eslint-plugin-next/image-component/no-unsized-images.test.js new file mode 100644 index 000000000000..615fb61c2401 --- /dev/null +++ b/test/eslint-plugin-next/image-component/no-unsized-images.test.js @@ -0,0 +1,115 @@ +const rule = require('@next/eslint-plugin-next/lib/rules/image-component/no-unsized-images.js') +const RuleTester = require('eslint').RuleTester + +RuleTester.setDefaultConfig({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + modules: true, + jsx: true, + }, + }, +}) + +var ruleTester = new RuleTester() +ruleTester.run('no-unsized-images', rule, { + valid: [ + ` + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, + ` + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, + ` + import MyImageName from 'next/image' + import Image from 'otherImage' + export default function Page() { + return
+ +
+ } + `, + ` + import MyImageName from 'next/image' + export default function Page() { + return
+ +
+ } + `, + ], + invalid: [ + { + code: ` + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, + errors: [ + { + messageId: 'unsizedImages', + }, + ], + }, + { + code: ` + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, + errors: [ + { + messageId: 'unsizedImages', + }, + ], + }, + { + code: ` + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, + errors: [ + { + messageId: 'unsizedImages', + }, + ], + }, + { + code: ` + import MyImageName from 'next/image' + import Image from 'otherImage' + export default function Page() { + return
+ +
+ } + `, + errors: [ + { + messageId: 'unsizedImages', + }, + ], + }, + ], +}) From 1b13c25aafc2886424660bc0e23a1833464fa711 Mon Sep 17 00:00:00 2001 From: atcastle Date: Tue, 6 Oct 2020 14:27:25 -0700 Subject: [PATCH 2/8] lint rule and test for missing alt text --- .../rules/image-component/missing-alt-text.js | 38 ++++++++++ .../image-component/missing-alt-text.test.js | 69 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 packages/eslint-plugin-next/lib/rules/image-component/missing-alt-text.js create mode 100644 test/eslint-plugin-next/image-component/missing-alt-text.test.js diff --git a/packages/eslint-plugin-next/lib/rules/image-component/missing-alt-text.js b/packages/eslint-plugin-next/lib/rules/image-component/missing-alt-text.js new file mode 100644 index 000000000000..7d1b56bc311f --- /dev/null +++ b/packages/eslint-plugin-next/lib/rules/image-component/missing-alt-text.js @@ -0,0 +1,38 @@ +module.exports = { + meta: { + messages: { + missingAltText: + 'All images should have an alt property for descriptive text. ' + + 'Purely decorative images can use an empty alt attribute.' + + 'See: https://web.dev/image-alt/', + }, + }, + create(context) { + let imageComponent = null + return { + ImportDeclaration(node) { + if (node.source.value === 'next/image') { + imageComponent = node.specifiers[0].local.name + } + }, + JSXOpeningElement(node) { + if (!imageComponent) { + return + } + if (node.name.name === imageComponent) { + if (!imageHasAltText(node)) { + context.report({ + node, + messageId: 'missingAltText', + }) + } + } + }, + } + }, +} + +function imageHasAltText(node) { + let attributes = node.attributes.map((attribute) => attribute.name.name) + return attributes.includes('alt') +} diff --git a/test/eslint-plugin-next/image-component/missing-alt-text.test.js b/test/eslint-plugin-next/image-component/missing-alt-text.test.js new file mode 100644 index 000000000000..3529572c9c4c --- /dev/null +++ b/test/eslint-plugin-next/image-component/missing-alt-text.test.js @@ -0,0 +1,69 @@ +const rule = require('@next/eslint-plugin-next/lib/rules/image-component/missing-alt-text.js') +const RuleTester = require('eslint').RuleTester + +RuleTester.setDefaultConfig({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + modules: true, + jsx: true, + }, + }, +}) + +var ruleTester = new RuleTester() +ruleTester.run('missing-alt-text', rule, { + valid: [ + ` + import Image from 'next/image' + export default function Page() { + return
+ A picture of foo +
+ } + `, + ` + import MyImageName from 'next/image' + import Image from 'otherImage' + export default function Page() { + return
+ +
+ } + `, + ], + invalid: [ + { + code: ` + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, + errors: [ + { + messageId: 'missingAltText', + }, + ], + }, + { + code: ` + import MyImageName from 'next/image' + import Image from 'otherImage' + export default function Page() { + return
+ +
+ } + `, + errors: [ + { + messageId: 'missingAltText', + }, + ], + }, + ], +}) From 58dca0c52fb3d9e0f2c6f01c3a954fefd9621356 Mon Sep 17 00:00:00 2001 From: atcastle Date: Tue, 6 Oct 2020 16:31:35 -0700 Subject: [PATCH 3/8] Refactor and format image component eslint tests --- .../rules/image-component/missing-alt-text.js | 31 ++--- .../image-component/no-unsized-images.js | 32 ++--- .../lib/utils/imageComponentRule.js | 23 ++++ .../image-component/missing-alt-text.test.js | 58 ++++----- .../image-component/no-unsized-images.test.js | 116 +++++++++--------- 5 files changed, 128 insertions(+), 132 deletions(-) create mode 100644 packages/eslint-plugin-next/lib/utils/imageComponentRule.js diff --git a/packages/eslint-plugin-next/lib/rules/image-component/missing-alt-text.js b/packages/eslint-plugin-next/lib/rules/image-component/missing-alt-text.js index 7d1b56bc311f..80557fa1ebf7 100644 --- a/packages/eslint-plugin-next/lib/rules/image-component/missing-alt-text.js +++ b/packages/eslint-plugin-next/lib/rules/image-component/missing-alt-text.js @@ -1,3 +1,5 @@ +const createImageRule = require('../../utils/imageComponentRule.js') + module.exports = { meta: { messages: { @@ -7,29 +9,14 @@ module.exports = { 'See: https://web.dev/image-alt/', }, }, - create(context) { - let imageComponent = null - return { - ImportDeclaration(node) { - if (node.source.value === 'next/image') { - imageComponent = node.specifiers[0].local.name - } - }, - JSXOpeningElement(node) { - if (!imageComponent) { - return - } - if (node.name.name === imageComponent) { - if (!imageHasAltText(node)) { - context.report({ - node, - messageId: 'missingAltText', - }) - } - } - }, + create: createImageRule((context, node) => { + if (!imageHasAltText(node)) { + context.report({ + node, + messageId: 'missingAltText', + }) } - }, + }), } function imageHasAltText(node) { diff --git a/packages/eslint-plugin-next/lib/rules/image-component/no-unsized-images.js b/packages/eslint-plugin-next/lib/rules/image-component/no-unsized-images.js index a5ef2ef3ffc8..9fd49cb7cd12 100644 --- a/packages/eslint-plugin-next/lib/rules/image-component/no-unsized-images.js +++ b/packages/eslint-plugin-next/lib/rules/image-component/no-unsized-images.js @@ -1,3 +1,5 @@ +const createImageRule = require('../../utils/imageComponentRule.js') + module.exports = { meta: { messages: { @@ -8,30 +10,14 @@ module.exports = { 'More info: https://web.dev/optimize-cls/#images-without-dimensions', }, }, - create(context) { - let imageComponent = null - return { - ImportDeclaration(node) { - if (node.source.value === 'next/image') { - imageComponent = node.specifiers[0].local.name - } - }, - JSXOpeningElement(node) { - if (!imageComponent) { - return - } - if (node.name.name === imageComponent) { - //Image Component rules here - if (!imageValidSize(node)) { - context.report({ - node, - messageId: 'unsizedImages', - }) - } - } - }, + create: createImageRule((context, node) => { + if (!imageValidSize(node)) { + context.report({ + node, + messageId: 'unsizedImages', + }) } - }, + }), } function imageValidSize(node) { diff --git a/packages/eslint-plugin-next/lib/utils/imageComponentRule.js b/packages/eslint-plugin-next/lib/utils/imageComponentRule.js new file mode 100644 index 000000000000..40706f0bd761 --- /dev/null +++ b/packages/eslint-plugin-next/lib/utils/imageComponentRule.js @@ -0,0 +1,23 @@ +// Factory for creating ESLint rules that identify the JSX Elements representing +// the 'next/image' component, and runs some check on those instances. + +module.exports = function (callback) { + return function (context) { + let imageComponent = null + return { + ImportDeclaration(node) { + if (node.source.value === 'next/image') { + imageComponent = node.specifiers[0].local.name + } + }, + JSXOpeningElement(node) { + if (!imageComponent) { + return + } + if (node.name.name === imageComponent) { + callback(context, node) + } + }, + } + } +} diff --git a/test/eslint-plugin-next/image-component/missing-alt-text.test.js b/test/eslint-plugin-next/image-component/missing-alt-text.test.js index 3529572c9c4c..a8ab6c178344 100644 --- a/test/eslint-plugin-next/image-component/missing-alt-text.test.js +++ b/test/eslint-plugin-next/image-component/missing-alt-text.test.js @@ -16,33 +16,33 @@ var ruleTester = new RuleTester() ruleTester.run('missing-alt-text', rule, { valid: [ ` - import Image from 'next/image' - export default function Page() { - return
- A picture of foo -
- } - `, + import Image from 'next/image' + export default function Page() { + return
+ A picture of foo +
+ } + `, ` - import MyImageName from 'next/image' - import Image from 'otherImage' - export default function Page() { - return
- -
- } + import MyImageName from 'next/image' + import Image from 'otherImage' + export default function Page() { + return
+ +
+ } `, ], invalid: [ { code: ` - import Image from 'next/image' - export default function Page() { - return
- -
- } - `, + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, errors: [ { messageId: 'missingAltText', @@ -51,14 +51,14 @@ ruleTester.run('missing-alt-text', rule, { }, { code: ` - import MyImageName from 'next/image' - import Image from 'otherImage' - export default function Page() { - return
- -
- } - `, + import MyImageName from 'next/image' + import Image from 'otherImage' + export default function Page() { + return
+ +
+ } + `, errors: [ { messageId: 'missingAltText', diff --git a/test/eslint-plugin-next/image-component/no-unsized-images.test.js b/test/eslint-plugin-next/image-component/no-unsized-images.test.js index 615fb61c2401..53342f0d142b 100644 --- a/test/eslint-plugin-next/image-component/no-unsized-images.test.js +++ b/test/eslint-plugin-next/image-component/no-unsized-images.test.js @@ -16,49 +16,49 @@ var ruleTester = new RuleTester() ruleTester.run('no-unsized-images', rule, { valid: [ ` - import Image from 'next/image' - export default function Page() { - return
- -
- } - `, + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, ` - import Image from 'next/image' - export default function Page() { - return
- -
- } - `, + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, ` - import MyImageName from 'next/image' - import Image from 'otherImage' - export default function Page() { - return
- -
- } - `, + import MyImageName from 'next/image' + import Image from 'otherImage' + export default function Page() { + return
+ +
+ } + `, ` - import MyImageName from 'next/image' - export default function Page() { - return
- -
- } - `, + import MyImageName from 'next/image' + export default function Page() { + return
+ +
+ } + `, ], invalid: [ { code: ` - import Image from 'next/image' - export default function Page() { - return
- -
- } - `, + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, errors: [ { messageId: 'unsizedImages', @@ -67,13 +67,13 @@ ruleTester.run('no-unsized-images', rule, { }, { code: ` - import Image from 'next/image' - export default function Page() { - return
- -
- } - `, + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, errors: [ { messageId: 'unsizedImages', @@ -82,13 +82,13 @@ ruleTester.run('no-unsized-images', rule, { }, { code: ` - import Image from 'next/image' - export default function Page() { - return
- -
- } - `, + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, errors: [ { messageId: 'unsizedImages', @@ -97,14 +97,14 @@ ruleTester.run('no-unsized-images', rule, { }, { code: ` - import MyImageName from 'next/image' - import Image from 'otherImage' - export default function Page() { - return
- -
- } - `, + import MyImageName from 'next/image' + import Image from 'otherImage' + export default function Page() { + return
+ +
+ } + `, errors: [ { messageId: 'unsizedImages', From 67f0e1a8d0b08a13e97394a3bdd70041d711c932 Mon Sep 17 00:00:00 2001 From: atcastle Date: Tue, 6 Oct 2020 16:31:59 -0700 Subject: [PATCH 4/8] Add eslint test for image componentt no-absolute-paths --- .../image-component/no-absolute-paths.js | 35 +++++++++ .../image-component/no-absolute-paths.test.js | 77 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 packages/eslint-plugin-next/lib/rules/image-component/no-absolute-paths.js create mode 100644 test/eslint-plugin-next/image-component/no-absolute-paths.test.js diff --git a/packages/eslint-plugin-next/lib/rules/image-component/no-absolute-paths.js b/packages/eslint-plugin-next/lib/rules/image-component/no-absolute-paths.js new file mode 100644 index 000000000000..2d392cba0ad1 --- /dev/null +++ b/packages/eslint-plugin-next/lib/rules/image-component/no-absolute-paths.js @@ -0,0 +1,35 @@ +const createImageRule = require('../../utils/imageComponentRule.js') + +module.exports = { + meta: { + messages: { + noAbsolutePaths: + 'You are using an absolute path in the src attribute of the next/image component.' + + 'This is almost definitely a mistake--use the "unoptimized" attribute to use ' + + 'an absolute path with no loader or optimizations.', + }, + }, + create: createImageRule((context, node) => { + if (hasBadAbsolutePath(node)) { + context.report({ + node, + messageId: 'noAbsolutePaths', + }) + } + }), +} +function hasBadAbsolutePath(node) { + let attributesObj = {} + node.attributes.forEach((attribute) => { + if (!attribute.value) { + attributesObj[attribute.name.name] = 1 + } else { + attributesObj[attribute.name.name] = attribute.value.value + } + }) + return ( + !attributesObj['unoptimized'] && + attributesObj.src && + attributesObj.src.match(/^http|www/) + ) +} diff --git a/test/eslint-plugin-next/image-component/no-absolute-paths.test.js b/test/eslint-plugin-next/image-component/no-absolute-paths.test.js new file mode 100644 index 000000000000..e8f23235b1ee --- /dev/null +++ b/test/eslint-plugin-next/image-component/no-absolute-paths.test.js @@ -0,0 +1,77 @@ +const rule = require('@next/eslint-plugin-next/lib/rules/image-component/no-absolute-paths.js') +const RuleTester = require('eslint').RuleTester + +RuleTester.setDefaultConfig({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + modules: true, + jsx: true, + }, + }, +}) + +var ruleTester = new RuleTester() +ruleTester.run('no-absolute-paths', rule, { + valid: [ + ` + import Image from 'next/image' + export default function Page() { + return
+ A picture of foo +
+ } + `, + ` + import Image from 'next/image' + export default function Page() { + return
+ A picture of foo +
+ } + `, + ` + import MyImageName from 'next/image' + import Image from 'otherImage' + export default function Page() { + return
+ +
+ } + `, + ], + invalid: [ + { + code: ` + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, + errors: [ + { + messageId: 'noAbsolutePaths', + }, + ], + }, + { + code: ` + import MyImageName from 'next/image' + import Image from 'otherImage' + export default function Page() { + return
+ +
+ } + `, + errors: [ + { + messageId: 'noAbsolutePaths', + }, + ], + }, + ], +}) From 03e673e79b2de869d1017bf20e45f0ef715b30ca Mon Sep 17 00:00:00 2001 From: atcastle Date: Tue, 6 Oct 2020 16:50:08 -0700 Subject: [PATCH 5/8] Add no-unoptimized-relative lint rule and test --- .../no-unoptimized-relative.js | 34 ++++++++ .../no-unoptimized-relative.js | 77 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 packages/eslint-plugin-next/lib/rules/image-component/no-unoptimized-relative.js create mode 100644 test/eslint-plugin-next/image-component/no-unoptimized-relative.js diff --git a/packages/eslint-plugin-next/lib/rules/image-component/no-unoptimized-relative.js b/packages/eslint-plugin-next/lib/rules/image-component/no-unoptimized-relative.js new file mode 100644 index 000000000000..dcacd428cb18 --- /dev/null +++ b/packages/eslint-plugin-next/lib/rules/image-component/no-unoptimized-relative.js @@ -0,0 +1,34 @@ +const createImageRule = require('../../utils/imageComponentRule.js') + +module.exports = { + meta: { + messages: { + noUnoptimizedRelative: + 'You are using arelaive path in the src attribute of the next/image component ' + + 'with the "unoptimized" attribute. Use absolute path or remove "unoptimized."', + }, + }, + create: createImageRule((context, node) => { + if (hasBadRelativePath(node)) { + context.report({ + node, + messageId: 'noUnoptimizedRelative', + }) + } + }), +} +function hasBadRelativePath(node) { + let attributesObj = {} + node.attributes.forEach((attribute) => { + if (!attribute.value) { + attributesObj[attribute.name.name] = 1 + } else { + attributesObj[attribute.name.name] = attribute.value.value + } + }) + return ( + attributesObj['unoptimized'] && + attributesObj.src && + !attributesObj.src.match(/^http|www/) + ) +} diff --git a/test/eslint-plugin-next/image-component/no-unoptimized-relative.js b/test/eslint-plugin-next/image-component/no-unoptimized-relative.js new file mode 100644 index 000000000000..2fc9692fae47 --- /dev/null +++ b/test/eslint-plugin-next/image-component/no-unoptimized-relative.js @@ -0,0 +1,77 @@ +const rule = require('@next/eslint-plugin-next/lib/rules/image-component/no-unoptimized-relative.js') +const RuleTester = require('eslint').RuleTester + +RuleTester.setDefaultConfig({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + modules: true, + jsx: true, + }, + }, +}) + +var ruleTester = new RuleTester() +ruleTester.run('no-unoptimized-relative', rule, { + valid: [ + ` + import Image from 'next/image' + export default function Page() { + return
+ A picture of foo +
+ } + `, + ` + import Image from 'next/image' + export default function Page() { + return
+ A picture of foo +
+ } + `, + ` + import MyImageName from 'next/image' + import Image from 'otherImage' + export default function Page() { + return
+ +
+ } + `, + ], + invalid: [ + { + code: ` + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, + errors: [ + { + messageId: 'noUnoptimizedRelative', + }, + ], + }, + { + code: ` + import MyImageName from 'next/image' + import Image from 'otherImage' + export default function Page() { + return
+ +
+ } + `, + errors: [ + { + messageId: 'noUnoptimizedRelative', + }, + ], + }, + ], +}) From d352e39899571f6c62b4da1b12a5c39385c373c4 Mon Sep 17 00:00:00 2001 From: atcastle Date: Wed, 7 Oct 2020 09:36:56 -0700 Subject: [PATCH 6/8] Add image component eslint rules to plugin --- packages/eslint-plugin-next/lib/index.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/eslint-plugin-next/lib/index.js b/packages/eslint-plugin-next/lib/index.js index 07501954c2ef..bf89bcc0b172 100644 --- a/packages/eslint-plugin-next/lib/index.js +++ b/packages/eslint-plugin-next/lib/index.js @@ -5,6 +5,10 @@ module.exports = { 'no-html-link-for-pages': require('./rules/no-html-link-for-pages'), 'no-unwanted-polyfillio': require('./rules/no-unwanted-polyfillio'), 'missing-preload': require('./rules/missing-preload'), + 'missing-alt-text': require('./rules/image-component/missing-alt-text'), + 'no-absolute-paths': require('./rules/image-component/no-absolute-paths'), + 'no-unoptimized-relative': require('./rules/image-component/no-unoptimized-relative'), + 'no-unsized-images': require('./rules/image-component/no-unsized-images'), }, configs: { recommended: { @@ -15,6 +19,10 @@ module.exports = { '@next/next/no-html-link-for-pages': 1, '@next/next/no-unwanted-polyfillio': 1, '@next/next/missing-preload': 1, + '@next/next/missing-alt-text': 1, + '@next/next/no-absolute-paths': 1, + '@next/next/no-unoptimized-relative': 1, + '@next/next/no-unsized-images': 1, }, }, }, From 81a5a7459f683cb9bfb0ec3235869553d13506e1 Mon Sep 17 00:00:00 2001 From: atcastle Date: Mon, 12 Oct 2020 17:03:48 -0700 Subject: [PATCH 7/8] Refactor attribute inspection logic into separate util, address feedback --- .../rules/image-component/missing-alt-text.js | 5 ++- .../image-component/no-absolute-paths.js | 17 +++------ .../no-unoptimized-relative.js | 16 +++----- .../lib/utils/nodeAttributes.js | 37 +++++++++++++++++++ .../image-component/no-absolute-paths.test.js | 8 ++++ ...ive.js => no-unoptimized-relative.test.js} | 0 6 files changed, 59 insertions(+), 24 deletions(-) create mode 100644 packages/eslint-plugin-next/lib/utils/nodeAttributes.js rename test/eslint-plugin-next/image-component/{no-unoptimized-relative.js => no-unoptimized-relative.test.js} (100%) diff --git a/packages/eslint-plugin-next/lib/rules/image-component/missing-alt-text.js b/packages/eslint-plugin-next/lib/rules/image-component/missing-alt-text.js index 80557fa1ebf7..9ed1b850f917 100644 --- a/packages/eslint-plugin-next/lib/rules/image-component/missing-alt-text.js +++ b/packages/eslint-plugin-next/lib/rules/image-component/missing-alt-text.js @@ -1,4 +1,5 @@ const createImageRule = require('../../utils/imageComponentRule.js') +const NodeAttributes = require('../../utils/nodeAttributes.js') module.exports = { meta: { @@ -20,6 +21,6 @@ module.exports = { } function imageHasAltText(node) { - let attributes = node.attributes.map((attribute) => attribute.name.name) - return attributes.includes('alt') + let attributes = new NodeAttributes(node) + return attributes.has('alt') } diff --git a/packages/eslint-plugin-next/lib/rules/image-component/no-absolute-paths.js b/packages/eslint-plugin-next/lib/rules/image-component/no-absolute-paths.js index 2d392cba0ad1..eb02abd44372 100644 --- a/packages/eslint-plugin-next/lib/rules/image-component/no-absolute-paths.js +++ b/packages/eslint-plugin-next/lib/rules/image-component/no-absolute-paths.js @@ -1,4 +1,5 @@ const createImageRule = require('../../utils/imageComponentRule.js') +const NodeAttributes = require('../../utils/nodeAttributes.js') module.exports = { meta: { @@ -19,17 +20,11 @@ module.exports = { }), } function hasBadAbsolutePath(node) { - let attributesObj = {} - node.attributes.forEach((attribute) => { - if (!attribute.value) { - attributesObj[attribute.name.name] = 1 - } else { - attributesObj[attribute.name.name] = attribute.value.value - } - }) + let attributes = new NodeAttributes(node) return ( - !attributesObj['unoptimized'] && - attributesObj.src && - attributesObj.src.match(/^http|www/) + !attributes.has('unoptimized') && + attributes.has('src') && + typeof attributes.value('src') === 'string' && + attributes.value('src').match(/^[a-zA-z\d]*:*\/\//) ) } diff --git a/packages/eslint-plugin-next/lib/rules/image-component/no-unoptimized-relative.js b/packages/eslint-plugin-next/lib/rules/image-component/no-unoptimized-relative.js index dcacd428cb18..9267adcc78af 100644 --- a/packages/eslint-plugin-next/lib/rules/image-component/no-unoptimized-relative.js +++ b/packages/eslint-plugin-next/lib/rules/image-component/no-unoptimized-relative.js @@ -1,4 +1,5 @@ const createImageRule = require('../../utils/imageComponentRule.js') +const NodeAttributes = require('../../utils/nodeAttributes.js') module.exports = { meta: { @@ -18,17 +19,10 @@ module.exports = { }), } function hasBadRelativePath(node) { - let attributesObj = {} - node.attributes.forEach((attribute) => { - if (!attribute.value) { - attributesObj[attribute.name.name] = 1 - } else { - attributesObj[attribute.name.name] = attribute.value.value - } - }) + let attributes = new NodeAttributes(node) return ( - attributesObj['unoptimized'] && - attributesObj.src && - !attributesObj.src.match(/^http|www/) + attributes.has('unoptimized') && + attributes.has('src') && + !attributes.value('src').match(/^[a-zA-z\d]*:*\/\//) ) } diff --git a/packages/eslint-plugin-next/lib/utils/nodeAttributes.js b/packages/eslint-plugin-next/lib/utils/nodeAttributes.js new file mode 100644 index 000000000000..b7d2bf6663c1 --- /dev/null +++ b/packages/eslint-plugin-next/lib/utils/nodeAttributes.js @@ -0,0 +1,37 @@ +// Return attributes and values of a node in a convenient way: +/* example: + + { attr1: { + hasValue: true, + value: 15 + }, + attr2: { + hasValue: false + } +Inclusion of hasValue is in case an eslint rule cares about boolean values +explicitely assigned to attribute vs the attribute being used as a flag +*/ +class NodeAttributes { + constructor(ASTnode) { + this.attributes = {} + ASTnode.attributes.forEach((attribute) => { + this.attributes[attribute.name.name] = { + hasValue: !!attribute.value, + } + if (attribute.value) { + this.attributes[attribute.name.name].value = attribute.value.value + } + }) + } + has(attrName) { + return !!this.attributes[attrName] + } + value(attrName) { + if (!this.attributes[attrName]) { + return true + } + return this.attributes[attrName].value + } +} + +module.exports = NodeAttributes diff --git a/test/eslint-plugin-next/image-component/no-absolute-paths.test.js b/test/eslint-plugin-next/image-component/no-absolute-paths.test.js index e8f23235b1ee..d289a1cd0445 100644 --- a/test/eslint-plugin-next/image-component/no-absolute-paths.test.js +++ b/test/eslint-plugin-next/image-component/no-absolute-paths.test.js @@ -21,6 +21,14 @@ ruleTester.run('no-absolute-paths', rule, { return
A picture of foo
+ } + `, + ` + import Image from 'next/image' + export default function Page() { + return
+ A picture of foo +
} `, ` diff --git a/test/eslint-plugin-next/image-component/no-unoptimized-relative.js b/test/eslint-plugin-next/image-component/no-unoptimized-relative.test.js similarity index 100% rename from test/eslint-plugin-next/image-component/no-unoptimized-relative.js rename to test/eslint-plugin-next/image-component/no-unoptimized-relative.test.js From 28ac11e08525fdf0f00757c7c5cbdce7a473f404 Mon Sep 17 00:00:00 2001 From: atcastle Date: Mon, 12 Oct 2020 17:24:10 -0700 Subject: [PATCH 8/8] Refactor unsized images test to use util and catch extra edge cases --- .../image-component/no-unsized-images.js | 13 ++++++-- .../lib/utils/nodeAttributes.js | 3 ++ .../image-component/no-unsized-images.test.js | 30 +++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin-next/lib/rules/image-component/no-unsized-images.js b/packages/eslint-plugin-next/lib/rules/image-component/no-unsized-images.js index 9fd49cb7cd12..7fdffb82a2ee 100644 --- a/packages/eslint-plugin-next/lib/rules/image-component/no-unsized-images.js +++ b/packages/eslint-plugin-next/lib/rules/image-component/no-unsized-images.js @@ -1,4 +1,5 @@ const createImageRule = require('../../utils/imageComponentRule.js') +const NodeAttributes = require('../../utils/nodeAttributes.js') module.exports = { meta: { @@ -21,9 +22,15 @@ module.exports = { } function imageValidSize(node) { - let attributes = node.attributes.map((attribute) => attribute.name.name) + let attributes = new NodeAttributes(node) + // Need to check both hasValue and value to catch attribute without value and + // attribute with empty string for value return ( - attributes.includes('unsized') || - (attributes.includes('height') && attributes.includes('width')) + attributes.has('unsized') || + (attributes.has('height') && + attributes.has('width') && + attributes.hasValue('height') && + attributes.hasValue('width') ** attributes.value('height') && + attributes.value('width')) ) } diff --git a/packages/eslint-plugin-next/lib/utils/nodeAttributes.js b/packages/eslint-plugin-next/lib/utils/nodeAttributes.js index b7d2bf6663c1..53185ca405d8 100644 --- a/packages/eslint-plugin-next/lib/utils/nodeAttributes.js +++ b/packages/eslint-plugin-next/lib/utils/nodeAttributes.js @@ -26,6 +26,9 @@ class NodeAttributes { has(attrName) { return !!this.attributes[attrName] } + hasValue(attrName) { + return !!this.attributes[attrName].hasValue + } value(attrName) { if (!this.attributes[attrName]) { return true diff --git a/test/eslint-plugin-next/image-component/no-unsized-images.test.js b/test/eslint-plugin-next/image-component/no-unsized-images.test.js index 53342f0d142b..5e041af7bbe2 100644 --- a/test/eslint-plugin-next/image-component/no-unsized-images.test.js +++ b/test/eslint-plugin-next/image-component/no-unsized-images.test.js @@ -80,6 +80,36 @@ ruleTester.run('no-unsized-images', rule, { }, ], }, + { + code: ` + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, + errors: [ + { + messageId: 'unsizedImages', + }, + ], + }, + { + code: ` + import Image from 'next/image' + export default function Page() { + return
+ +
+ } + `, + errors: [ + { + messageId: 'unsizedImages', + }, + ], + }, { code: ` import Image from 'next/image'