Skip to content
Merged
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,5 @@
[
"Float64Array",
"Float32Array",
"Float16Array"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
"Int8Array",
"Uint8Array",
"Uint8ClampedArray",
"Int16Array",
"Uint16Array",
"Int32Array",
"Uint32Array"
]
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,32 @@
var parseJS = require( 'acorn' ).parse;
var parseJSDoc = require( 'doctrine' ).parse;
var contains = require( '@stdlib/assert/contains' );
var hasOwnProp = require( '@stdlib/assert/has-own-property' );
var isString = require( '@stdlib/assert/is-string' ).isPrimitive;
var isArray = require( '@stdlib/assert/is-array' );
Comment thread
kgryte marked this conversation as resolved.
var isEmptyArray = require( '@stdlib/assert/is-empty-array' );
var isObject = require( '@stdlib/assert/is-object' );
var isNull = require( '@stdlib/assert/is-null' );
var replace = require( '@stdlib/string/replace' );
var objectKeys = require( '@stdlib/utils/keys' );
var findJSDoc = require( '@stdlib/_tools/eslint/utils/find-jsdoc' );
var INTEGER_TYPES = require( './integer_types.json' );
var FLOAT_TYPES = require( './float_types.json' );
var INTEGER_ARRAY_TYPES = require( './integer_array_types.json' );
var FLOAT_ARRAY_TYPES = require( './float_array_types.json' );


// VARIABLES //

var RE_TRAILING_COMMENT = /; (\/\/|=>)[^\n]*\n/g;
var RE_ANNOTATION = /(?:returns|=>|throws) {0,1}([\s\S]*?)(?:\n\n|$)/;
var RE_NUMBER = /^[0-9e+-.]+$/;
var RE_NUMBER = /^[~0-9e+-.]+$/;
var RE_DECIMAL = /([~+-]*[0-9]+)(?:.0)?(e[+-]?[0-9]+)?/;
var RE_MUTATION = /^ *[a-zA-Z_$][\w$]*(?:\.[\w$]+|\[[^\]]*\])* *=>/;
var DOPTS = {
'sloppy': true,
'unwrap': true,
'tags': [ 'example', 'returns' ]
'tags': [ 'example', 'returns', 'param' ]
};
var rule;

Expand Down Expand Up @@ -80,6 +87,186 @@
return null;
}

/**
* Checks the element type of a typed array type for whether annotations for element values should include a decimal point.
*
* @private
* @param {string} type - type name
* @returns {(boolean|null)} for typed arrays, a boolean indicating whether annotations for element values should include a decimal point, `null` otherwise
*/
function checkArrayType( type ) {
if ( contains( FLOAT_ARRAY_TYPES, type ) ) {
return true;
}
if ( contains( INTEGER_ARRAY_TYPES, type ) ) {
return false;
}
return null;
}

/**
* Resolves the function AST node associated with a documented node.
*
* @private
* @param {ASTNode} node - documented node
* @returns {(ASTNode|null)} function AST node or null
*/
function functionNode( node ) {
var decls;
var expr;
var i;
if (
node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression'
) {
return node;
}
if ( node.type === 'VariableDeclaration' ) {
decls = node.declarations;
for ( i = 0; i < decls.length; i++ ) {
if (
decls[ i ].init &&
(
decls[ i ].init.type === 'FunctionExpression' ||
decls[ i ].init.type === 'ArrowFunctionExpression'
)
) {
return decls[ i ].init;
}
}
return null;
}
if ( node.type === 'ExpressionStatement' ) {
expr = node.expression;
if (
expr &&
expr.type === 'AssignmentExpression' &&
expr.right &&
(
expr.right.type === 'FunctionExpression' ||
expr.right.type === 'ArrowFunctionExpression'
)
) {
return expr.right;
}
}
return null;
}

/**
* Collects the return statements of a function body without descending into nested functions.
*
* @private
* @param {(ASTNode|Array)} node - AST node (or node list) to examine
* @param {Array} out - output array
* @returns {Array} output array
*/
function findReturnStatements( node, out ) {
var keys;
var v;
var i;
if ( isArray( node ) ) {
for ( i = 0; i < node.length; i++ ) {
findReturnStatements( node[ i ], out );
}
return out;
}
if ( !isObject( node ) || !isString( node.type ) ) {
return out;
}
if (
node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression'
) {
return out;
}
if ( node.type === 'ReturnStatement' ) {
out.push( node );
return out;
}
keys = objectKeys( node );
for ( i = 0; i < keys.length; i++ ) {
if ( keys[ i ] === 'parent' ) {
continue;
}
v = node[ keys[ i ] ];
if ( isArray( v ) || ( isObject( v ) && isString( v.type ) ) ) {
findReturnStatements( v, out );
}
}
return out;
}

/**
* Checks whether a documented function only ever returns elements of typed array parameters and, if so, whether annotations for returned values should include a decimal point.
*
* @private
* @param {ASTNode} node - documented node
* @param {Array} tags - JSDoc tags
* @returns {(boolean|null)} for typed array element accessors, a boolean indicating whether annotations for returned values should include a decimal point, `null` otherwise
*/
function checkReturnedElements( node, tags ) {
var returns;
var params;
var fnode;
var flg;
var arg;
var tag;
var f;
var i;

// Resolve the documented parameters which are typed arrays...
params = {};
for ( i = 0; i < tags.length; i++ ) {
tag = tags[ i ];
if ( tag.title === 'param' && tag.type && tag.type.name && tag.name ) {
f = checkArrayType( tag.type.name );
if ( !isNull( f ) ) {
params[ tag.name ] = f;
}
}
}
fnode = functionNode( node );
if ( isNull( fnode ) || !fnode.body ) {
return null;
}
if ( fnode.body.type === 'BlockStatement' ) {
returns = findReturnStatements( fnode.body, [] );
} else {
// Handle arrow functions having concise (expression) bodies by treating the body as the sole returned expression:
returns = [
{
'argument': fnode.body
}
];
}
if ( returns.length === 0 ) {
return null;
}
// Check whether every return statement returns an element of a typed array parameter...
flg = null;
for ( i = 0; i < returns.length; i++ ) {
arg = returns[ i ].argument;
if (
!arg ||
arg.type !== 'MemberExpression' ||
!arg.computed ||
arg.object.type !== 'Identifier' ||
!hasOwnProp( params, arg.object.name )
) {
return null;
}
f = params[ arg.object.name ];
if ( !isNull( flg ) && f !== flg ) {
return null;
}
flg = f;
}
return flg;
}

/**
* Checks whether a comment is a return annotation and, if so, whether it only includes decimal points for real-valued return values.
*
Expand All @@ -95,11 +282,19 @@
var val;

str = comment.value;
if ( RE_MUTATION.test( str ) ) {
// Annotations describing mutated values (e.g., `// D => [ 3.0 ]`) do not describe a function's return value:
return null;
}
matches = str.match( RE_ANNOTATION );
if ( matches ) {
val = matches[ 1 ];
if ( !RE_NUMBER.test( val ) ) {
// Annotation values which are not scalar numeric values (e.g., arrays, strings, objects, etc) do not have the documented return type:
return null;
}
if ( flg ) {
if ( RE_NUMBER.test( val ) && !contains( val, '.' ) ) {
if ( !contains( val, '.' ) ) {
return '`//'+str+'` should be `//'+createReplacement( str, flg )+'` (return annotations for values of type `'+rType+'` must always include a decimal point)';
}
}
Expand Down Expand Up @@ -146,6 +341,7 @@
var jsdoc;
var descr;
var rType;
var elems;
var tags;
var ast;
var msg;
Expand All @@ -160,18 +356,31 @@
if ( isEmptyArray( tags ) ) {
return;
}
if ( tags[ 0 ].title === 'returns' && tags[ 0 ].type ) {
rType = tags[ 0 ].type.name;
flg = checkType( rType );
if ( isNull( flg ) ) {
return;
rType = null;
for ( i = 0; i < tags.length; i++ ) {
if ( tags[ i ].title === 'returns' && tags[ i ].type ) {
rType = tags[ i ].type.name;
break;
}
} else {
}
if ( isNull( rType ) ) {
// Return early for functions that do not have a return value...
return;
}
for ( i = 1; i < tags.length; i++ ) {
flg = checkType( rType );
if ( isNull( flg ) ) {
return;
}
// Handle functions which return elements of typed array parameters (e.g., a function documented as returning a `number` may return integer-valued elements of a `Uint8Array`, in which case annotation values are ambiguous and should not be validated)...
elems = checkReturnedElements( node, tags );
if ( !isNull( elems ) && elems !== flg ) {
return;
}
for ( i = 0; i < tags.length; i++ ) {
tag = tags[ i ];
if ( tag.title !== 'example' ) {
continue;
}
comments = [];
descr = tag.description;

Expand Down Expand Up @@ -220,7 +429,7 @@
'docs': {
'description': 'enforce that (only) return annotations for floating-point typed values always contain decimal points'
},
'schema': []

Check warning on line 432 in lib/node_modules/@stdlib/_tools/eslint/rules/jsdoc-doctest-decimal-point/lib/main.js

View workflow job for this annotation

GitHub Actions / Lint Changed Files

File has too many lines (305). Maximum allowed is 300
},
'create': main
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,62 @@ test = {
};
invalid.push( test );

test = {
'code': [
'/**',
'* Returns an element from a `Float64Array`.',
'*',
'* @private',
'* @param {Float64Array} arr - input array',
'* @param {NonNegativeInteger} idx - element index',
'* @returns {number} element value',
'*',
'* @example',
'* var Float64Array = require( \'@stdlib/array/float64\' );',
'*',
'* var arr = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );',
'*',
'* var v = getFloat64( arr, 2 );',
'* // returns 3',
'*/',
'function getFloat64( arr, idx ) {',
' return arr[ idx ];',
'}'
].join( '\n' ),
'errors': [
{
'message': '`// returns 3` should be `// returns 3.0` (return annotations for values of type `number` must always include a decimal point)',
'type': null
}
]
};
invalid.push( test );

test = {
'code': [
'/**',
'* Returns the number of elements.',
'*',
'* @param {Collection} x - input collection',
'* @returns {integer} number of elements',
'*',
'* @example',
'* var n = numel( [ 1, 2, 3 ] );',
'* // returns ~3.0',
'*/',
'function numel( x ) {',
' return x.length;',
'}'
].join( '\n' ),
'errors': [
{
'message': '`// returns ~3.0` should be `// returns ~3` (return annotations for values of type `integer` must NOT include a decimal point)',
'type': null
}
]
};
invalid.push( test );


// EXPORTS //

Expand Down
Loading