Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend valid-sprintf ESLint rule to catch placeholders that should be numbered #20574

Merged
merged 54 commits into from
Apr 3, 2020
Merged
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
e8656b9
Add new ESLint rule to validate text domains
swissspidy Feb 28, 2020
80f6b8b
Enforce `@wordpress/valid-text-domain` rule for Gutenberg code base
swissspidy Feb 28, 2020
656ec12
Use messageId and make fixable
swissspidy Feb 29, 2020
6beedd1
Add new no-missing-translator-comments rule
swissspidy Feb 29, 2020
d366d59
Enforce `@wordpress/no-missing-translator-comments` rule for Gutenber…
swissspidy Feb 29, 2020
6c4e07c
Add docs
swissspidy Feb 29, 2020
f1b325b
Merge master branch and rename rules
swissspidy Mar 11, 2020
05ee583
Implement feedback from code review
swissspidy Mar 11, 2020
3af1fb5
Rename rule names
swissspidy Mar 11, 2020
82c956d
Simplify getting previousArg
swissspidy Mar 11, 2020
d611c85
Extract and document utils
swissspidy Mar 11, 2020
8194ffa
Combine comments
swissspidy Mar 11, 2020
be5a596
Merge branch 'master' into add/i18n-eslint-rules
swissspidy Mar 11, 2020
6125f28
Derive allowDefault from allowedTextDomains
swissspidy Mar 11, 2020
797053d
Merge branch 'master' into add/i18n-eslint-rules
swissspidy Mar 19, 2020
02e30c4
Break early for line number mismatches
swissspidy Mar 19, 2020
225c09d
Merge branch 'master' into add/i18n-eslint-rules
swissspidy Mar 19, 2020
11569f3
Support `i18n.*` usage in new rules
swissspidy Mar 19, 2020
d1b2fec
Merge branch 'master' into add/i18n-eslint-rules
swissspidy Mar 24, 2020
c48aa0a
Merge branch 'master' into add/i18n-eslint-rules
swissspidy Mar 25, 2020
9e535ce
Add new i18n-no-variables rule
swissspidy Mar 24, 2020
811c951
Add new i18n-ellipsis rule
swissspidy Mar 25, 2020
210759d
Add new i18n-no-placeholders-only rule
swissspidy Mar 25, 2020
4511fd2
Add new i18n-no-collapsible-whitespace rule
swissspidy Mar 25, 2020
0d8b751
Use messageId in valid-sprintf rule
swissspidy Mar 2, 2020
40fd705
Catch mix of ordered and non-ordered placeholders in valid-sprintf rule
swissspidy Mar 19, 2020
70401a4
Fix code base after change
swissspidy Mar 2, 2020
b0fba98
Mark as breaking change in the readme
swissspidy Mar 19, 2020
85d6fc2
Support `i18n.*` usage in `valid-sprintf` rule
swissspidy Mar 19, 2020
bee529e
Disable i18n-no-collapsible-whitespace rule for now
swissspidy Mar 26, 2020
b2fcda3
Merge branch 'master' into add/i18n-eslint-rules
swissspidy Mar 26, 2020
d388f13
Remove unneded capture group
swissspidy Mar 27, 2020
67e38e5
Use Set for list of translation functions
swissspidy Mar 27, 2020
545790b
Move const to top scope
swissspidy Mar 27, 2020
05a8920
Coding standards in code examples
swissspidy Mar 27, 2020
f845bae
Refactor utils to make code more DRY
swissspidy Mar 27, 2020
63c8606
Coding standards in test code
swissspidy Mar 27, 2020
63fd381
Remove now unneeded no-restricted-syntax config
swissspidy Mar 27, 2020
e6fe285
Add i18n rules to new i18n config
swissspidy Mar 27, 2020
5ceb0c7
Mark new ruleset as breaking change
swissspidy Mar 27, 2020
a2e16d5
Update docs
swissspidy Mar 27, 2020
4c2d632
Merge branch 'master' into add/i18n-eslint-rules
swissspidy Mar 27, 2020
5bcac71
Fix tests
swissspidy Mar 30, 2020
3c05054
Merge branch 'master' into add/i18n-eslint-rules
swissspidy Mar 30, 2020
e7187a0
Rename argument to allowedTextDomain and allow strings and arrays
swissspidy Mar 31, 2020
887ed2d
Merge branch 'master' into add/i18n-eslint-rules
swissspidy Apr 1, 2020
c70a4e8
Merge branch 'add/i18n-eslint-rules' into fix/eslint-plugin-valid-spr…
swissspidy Apr 1, 2020
b65c7e2
Merge branch 'master' into fix/eslint-plugin-valid-sprintf-rule
swissspidy Apr 2, 2020
e1c6ee1
Fix case when using object spread
swissspidy Apr 2, 2020
12e2843
Merge branch 'master' into fix/eslint-plugin-valid-sprintf-rule
swissspidy Apr 3, 2020
96bbd6f
Apply changes from code review
swissspidy Apr 3, 2020
1c14199
Rename messageId to make it more clear
swissspidy Apr 3, 2020
e6fc69c
Update regex to properly allow named arguments which are supported
swissspidy Apr 3, 2020
f0beb82
Update i18n-no-placeholders-only rule
swissspidy Apr 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions packages/blocks/src/api/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export function getAccessibleBlockLabel(
if ( hasPosition && direction === 'vertical' ) {
if ( hasLabel ) {
return sprintf(
/* translators: accessibility text. %1: The block title, %2: The block row number, %3: The block label.. */
/* translators: accessibility text. 1: The block title. 2: The block row number. 3: The block label.. */
__( '%1$s Block. Row %2$d. %3$s' ),
title,
position,
Expand All @@ -193,15 +193,15 @@ export function getAccessibleBlockLabel(
}

return sprintf(
/* translators: accessibility text. %s: The block title, %d The block row number. */
__( '%s Block. Row %d' ),
/* translators: accessibility text. 1: The block title. 2: The block row number. */
__( '%1$s Block. Row %2$d' ),
title,
position
);
} else if ( hasPosition && direction === 'horizontal' ) {
if ( hasLabel ) {
return sprintf(
/* translators: accessibility text. %1: The block title, %2: The block column number, %3: The block label.. */
/* translators: accessibility text. 1: The block title. 2: The block column number. 3: The block label.. */
__( '%1$s Block. Column %2$d. %3$s' ),
title,
position,
Expand All @@ -210,8 +210,8 @@ export function getAccessibleBlockLabel(
}

return sprintf(
/* translators: accessibility text. %s: The block title, %d The block column number. */
__( '%s Block. Column %d' ),
/* translators: accessibility text. 1: The block title. 2: The block column number. */
__( '%1$s Block. Column %2$d' ),
title,
position
);
Expand Down
7 changes: 6 additions & 1 deletion packages/eslint-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
### Breaking Changes

- There is a new `i18n` ruleset that includes all i18n-related rules and is included in the `recommended` ruleset.
- The `valid-sprintf` rule has been moved from the `custom` ruleset to the `i18n` ruleset.
- The `@wordpress/valid-sprintf` rule has been moved from the `custom` ruleset to the `i18n` ruleset.
- The `@wordpress/valid-sprintf` rule now recognizes mix of ordered and non-ordered placeholders.

### Bug Fix

- The `@wordpress/valid-sprintf` rule now detects usage of `sprintf` via `i18n.sprintf` (e.g. when using `import * as i18n from '@wordpress/i18n'`).

## 4.0.0 (2020-02-10)

Expand Down
97 changes: 52 additions & 45 deletions packages/eslint-plugin/rules/__tests__/valid-sprintf.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,76 +37,83 @@ ruleTester.run( 'valid-sprintf', rule, {
{
code: `var value = ''; sprintf( value, 'substitute' )`,
},
{
code: `
sprintf(
/* translators: 1: number of blocks. 2: average rating. */
_n(
'This author has %1$d block, with an average rating of %2$d.',
'This author has %1$d blocks, with an average rating of %2$d.',
authorBlockCount
),
authorBlockCount,
authorBlockRating
);`,
},
{
code: `i18n.sprintf( '%s', 'substitute' )`,
},
{
code: `i18n.sprintf( i18n.__( '%s' ), 'substitute' )`,
},
{
code: `sprintf( ...args )`,
},
],
invalid: [
{
code: `sprintf()`,
errors: [
{ message: 'sprintf must be called with a format string' },
],
errors: [ { messageId: 'noFormatString' } ],
},
{
code: `sprintf( '%s' )`,
errors: [
{
message:
'sprintf must be called with placeholder value argument(s)',
},
],
errors: [ { messageId: 'noPlaceholderArgs' } ],
},
{
code: `sprintf( 1, 'substitute' )`,
errors: [
{
message:
'sprintf must be called with a valid format string',
},
],
errors: [ { messageId: 'invalidFormatString' } ],
},
{
code: `sprintf( [], 'substitute' )`,
errors: [
{
message:
'sprintf must be called with a valid format string',
},
],
errors: [ { messageId: 'invalidFormatString' } ],
},
{
code: `sprintf( '%%', 'substitute' )`,
errors: [
{
message:
'sprintf format string must contain at least one placeholder',
},
],
errors: [ { messageId: 'noPlaceholders' } ],
},
{
code: `sprintf( __( '%%' ), 'substitute' )`,
errors: [
{
message:
'sprintf format string must contain at least one placeholder',
},
],
errors: [ { messageId: 'noPlaceholders' } ],
},
{
code: `sprintf( _n( '%s', '' ), 'substitute' )`,
errors: [
{
message:
'sprintf format string options must have the same number of placeholders',
},
],
errors: [ { messageId: 'placeholderMismatch' } ],
},
{
code: `sprintf( _n( '%s', '%s %s' ), 'substitute' )`,
errors: [
{
message:
'sprintf format string options must have the same number of placeholders',
},
],
errors: [ { messageId: 'placeholderMismatch' } ],
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
},
{
code: `
sprintf(
/* translators: 1: number of blocks. 2: average rating. */
_n(
'This author has %d block, with an average rating of %d.',
'This author has %d blocks, with an average rating of %d.',
authorBlockCount
),
authorBlockCount,
authorBlockRating
);`,
errors: [ { messageId: 'noNumberedPlaceholders' } ],
},
{
code: `i18n.sprintf()`,
errors: [ { messageId: 'noFormatString' } ],
},
{
code: `i18n.sprintf( i18n.__( '%%' ), 'substitute' )`,
errors: [ { messageId: 'noPlaceholders' } ],
},
],
} );
4 changes: 2 additions & 2 deletions packages/eslint-plugin/rules/i18n-no-placeholders-only.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
const {
TRANSLATION_FUNCTIONS,
REGEXP_PLACEHOLDER,
REGEXP_SPRINTF_PLACEHOLDER,
getTextContentFromNode,
getTranslateFunctionName,
getTranslateFunctionArgs,
Expand Down Expand Up @@ -41,7 +41,7 @@ module.exports = {
}

const modifiedString = argumentString.replace(
REGEXP_PLACEHOLDER,
REGEXP_SPRINTF_PLACEHOLDER,
''
);

Expand Down
6 changes: 3 additions & 3 deletions packages/eslint-plugin/rules/i18n-translator-comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
const {
TRANSLATION_FUNCTIONS,
REGEXP_PLACEHOLDER,
REGEXP_SPRINTF_PLACEHOLDER,
getTranslateFunctionName,
getTranslateFunctionArgs,
getTextContentFromNode,
Expand Down Expand Up @@ -45,10 +45,10 @@ module.exports = {
}

const hasPlaceholders = candidates.some( ( candidate ) =>
REGEXP_PLACEHOLDER.test( candidate )
REGEXP_SPRINTF_PLACEHOLDER.test( candidate )
);
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test#Using_test()_on_a_regex_with_the_global_flag.
REGEXP_PLACEHOLDER.lastIndex = 0;
REGEXP_SPRINTF_PLACEHOLDER.lastIndex = 0;

if ( ! hasPlaceholders ) {
return;
Expand Down
96 changes: 73 additions & 23 deletions packages/eslint-plugin/rules/valid-sprintf.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* Internal dependencies
*/
const {
REGEXP_PLACEHOLDER,
REGEXP_SPRINTF_PLACEHOLDER,
REGEXP_SPRINTF_PLACEHOLDER_UNORDERED,
getTranslateFunctionArgs,
getTextContentFromNode,
} = require( '../utils' );
Expand All @@ -11,28 +12,51 @@ module.exports = {
meta: {
type: 'problem',
schema: [],
messages: {
noFormatString: 'sprintf must be called with a format string',
invalidFormatString:
'sprintf must be called with a valid format string',
noPlaceholderArgs:
'sprintf must be called with placeholder value argument(s)',
noPlaceholders:
'sprintf format string must contain at least one placeholder',
placeholderMismatch:
'sprintf format string options must have the same number of placeholders',
noNumberedPlaceholders:
'Multiple sprintf placeholders should be ordered. Mix of ordered and non-ordered placeholders found.',
},
},
create( context ) {
return {
CallExpression( node ) {
const { callee, arguments: args } = node;
if ( callee.name !== 'sprintf' ) {

const functionName =
callee.property && callee.property.name
? callee.property.name
: callee.name;

if ( functionName !== 'sprintf' ) {
return;
}

if ( ! args.length ) {
context.report(
context.report( {
node,
'sprintf must be called with a format string'
);
messageId: 'noFormatString',
} );
return;
}

if ( args.length < 2 ) {
context.report(
if ( args[ 0 ].type === 'SpreadElement' ) {
return;
}

context.report( {
node,
'sprintf must be called with placeholder value argument(s)'
);
messageId: 'noPlaceholderArgs',
} );
return;
}

Expand All @@ -47,10 +71,16 @@ module.exports = {
break;

case 'CallExpression':
const argFunctionName =
args[ 0 ].callee.property &&
args[ 0 ].callee.property.name
? args[ 0 ].callee.property.name
: args[ 0 ].callee.name;
swissspidy marked this conversation as resolved.
Show resolved Hide resolved

// All possible options (arguments) from a translate
// function must be valid.
candidates = getTranslateFunctionArgs(
args[ 0 ].callee.name,
argFunctionName,
args[ 0 ].arguments,
false
).map( getTextContentFromNode );
Expand All @@ -75,44 +105,64 @@ module.exports = {
}

if ( ! candidates.length ) {
context.report(
context.report( {
node,
'sprintf must be called with a valid format string'
);
messageId: 'invalidFormatString',
} );
return;
}

let numPlaceholders;
for ( let i = 0; i < candidates.length; i++ ) {
const match = candidates[ i ].match( REGEXP_PLACEHOLDER );
for ( const candidate of candidates ) {
const allMatches = candidate.match(
REGEXP_SPRINTF_PLACEHOLDER
);
const unorderedMatches = candidate.match(
REGEXP_SPRINTF_PLACEHOLDER_UNORDERED
);
swissspidy marked this conversation as resolved.
Show resolved Hide resolved

// Prioritize placeholder number consistency over matching
// placeholder, since it's a more common error to omit a
// placeholder from the singular form of pluralization.
if (
numPlaceholders !== undefined &&
( ! match || numPlaceholders !== match.length )
( ! allMatches ||
numPlaceholders !== allMatches.length )
) {
context.report( {
node,
messageId: 'placeholderMismatch',
} );
return;
}

if (
unorderedMatches &&
allMatches &&
unorderedMatches.length > 0 &&
allMatches.length > 1 &&
unorderedMatches.length !== allMatches.length
) {
context.report(
context.report( {
node,
'sprintf format string options must have the same number of placeholders'
);
messageId: 'noNumberedPlaceholders',
} );
return;
}

if ( ! match ) {
context.report(
if ( ! allMatches ) {
context.report( {
node,
'sprintf format string must contain at least one placeholder'
);
messageId: 'noPlaceholders',
} );
return;
}

if ( numPlaceholders === undefined ) {
// Track the number of placeholders discovered in the
// string to verify that all other candidate options
// have the same number.
numPlaceholders = match.length;
numPlaceholders = allMatches.length;
}
}
},
Expand Down
Loading