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

ESLint Plugin: Introduce rule json-schema-no-plain-object-types #21687

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Require all "object" types within a block's attributes to define their expected properties (json-schema-no-plain-object-types)

Block attributes must conform to types as specified in the [WordPress REST API documentation](https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/), a behavior based on [JSON Schema](http://json-schema.org/). This schema is used to validate attribute data not only in the REST API, but [also in the block editor](https://developer.wordpress.org/block-editor/developers/block-api/block-attributes/#attribute-type-validation).

This rule requires that any declaration of an `object` type specify the exact properties that make up the object. This applies to the type of attributes themselves as well as any data within an attribute, such as when an attribute of type `array` expects values of type `object`.

## Rule details

Examples of **incorrect** code for this rule:

```js
{
"name": "my/gallery",
"category": "widgets",
"attributes": {
"images": {
"type": "object"
}
}
}
```

```js
{
"name": "my/gallery",
"category": "widgets",
"attributes": {
"images": {
"type": "array",
"items": {
"type": "object"
}
}
}
}
```

Examples of **correct** code for this rule:

```js
{
"name": "my/gallery",
"category": "widgets",
"attributes": {
"images": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"width": {
"type": "number"
}
}
}
}
}
}
```

```js
{
"name": "my/gallery",
"category": "widgets",
"attributes": {
"images": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"width": {
"type": "number"
}
},
"required": [
"id", "width"
],
"additionalProperties": false
}
}
}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* External dependencies
*/
import { RuleTester } from 'eslint';

/**
* Internal dependencies
*/
import rule from '../json-schema-no-plain-object-types';

const ruleTester = new RuleTester( {
parserOptions: {
ecmaVersion: 6,
},
} );

const UNSPECIFIED_OBJECT_ERROR =
"Any object within a block's attributes must have its properties defined.";

ruleTester.run( 'json-schema-no-plain-object-types', rule, {
valid: [
{
code: `
( {
"name": "my/gallery",
"category": "widgets",
"attributes": {
"images": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"width": {
"type": "number"
}
}
}
}
} )
`,
},
{
code: `
( {
"name": "my/gallery",
"category": "widgets",
"attributes": {
"images": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"width": {
"type": "number"
}
}
}
}
}
} )
`,
},
{
code: `
( {
"name": "my/gallery",
"category": "widgets",
"attributes": {
"images": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"width": {
"type": "number"
}
},
"required": [
"id", "width"
],
"additionalProperties": false
}
}
}
} )
`,
},
],
invalid: [
{
code: `
( {
"name": "my/gallery",
"category": "widgets",
"attributes": {
"images": {
"type": "array",
"items": {
"type": "object"
}
}
}
} )
`,
errors: [ { message: UNSPECIFIED_OBJECT_ERROR } ],
},
{
code: `
( {
"name": "my/gallery",
"category": "widgets",
"attributes": {
"images": {
"type": "object"
}
}
} )
`,
errors: [ { message: UNSPECIFIED_OBJECT_ERROR } ],
},
],
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const UNSPECIFIED_OBJECT_ERROR =
"Any object within a block's attributes must have its properties defined.";

/**
* Traverse up through the chain of parent AST nodes returning the first parent
* the predicate returns a truthy value for.
*
* @param {Object} sourceNode The AST node to search from.
* @param {Function} predicate A predicate invoked for each parent.
*
* @return {?Object } The first encountered parent node where the predicate
* returns a truthy value.
*/
function findParent( sourceNode, predicate ) {
if ( ! sourceNode.parent ) {
return;
}

if ( predicate( sourceNode.parent ) ) {
return sourceNode.parent;
}

return findParent( sourceNode.parent, predicate );
}

/**
* Please edit me
*
* @param {Object} node The ObjectExpression containing a `type` property.
* @param {Object} context The eslint context object.
*/
function testIsNotPlainObjectType( node, context ) {
const jsonSchemaPropertiesDeclaration = node.properties.find(
( candidate ) =>
candidate.key.type === 'Literal' &&
candidate.key.value === 'properties'
);

if ( ! jsonSchemaPropertiesDeclaration ) {
context.report( node, UNSPECIFIED_OBJECT_ERROR );
}
}

module.exports = {
meta: {
type: 'problem',
schema: [],
},
create( context ) {
return {
Literal( node ) {
// Bypass any object property that isn't `type: 'object'`.
if (
node.value !== 'object' ||
! ( node.parent && node.parent.type === 'Property' ) ||
! (
node.parent.key.type === 'Literal' &&
node.parent.key.value === 'type'
)
) {
return;
}

// Capture the object expression in which property `type:
// 'object'` was found.
const attributeObjectExpression = node.parent.parent;

// Bypass any object expression that isn't nested in the value of
// a property of key `attributes`.
const attributesPropertyAncestor = findParent(
attributeObjectExpression,
( candidate ) =>
candidate.type === 'Property' &&
candidate.key.type === 'Literal' &&
candidate.key.value === 'attributes'
);
if ( ! attributesPropertyAncestor ) {
return;
}

testIsNotPlainObjectType( attributeObjectExpression, context );
},
};
},
};