Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher committed Jan 29, 2020
1 parent c5f4881 commit cde59a5
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 133 deletions.
132 changes: 1 addition & 131 deletions packages/eslint-plugin/src/rules/prefer-readonly-parameter-types.ts
Expand Up @@ -2,8 +2,6 @@ import {
TSESTree,
AST_NODE_TYPES,
} from '@typescript-eslint/experimental-utils';
import { isObjectType, isUnionType, unionTypeParts } from 'tsutils';
import * as ts from 'typescript';
import * as util from '../util';

export default util.createRule({
Expand Down Expand Up @@ -31,134 +29,6 @@ export default util.createRule({
const { esTreeNodeToTSNodeMap, program } = util.getParserServices(context);
const checker = program.getTypeChecker();

/**
* Returns:
* - null if the type is not an array or tuple,
* - true if the type is a readonly array or readonly tuple,
* - false if the type is a mutable array or mutable tuple.
*/
function isTypeReadonlyArrayOrTuple(type: ts.Type): boolean | null {
function checkTypeArguments(arrayType: ts.TypeReference): boolean {
const typeArguments = checker.getTypeArguments(arrayType);
if (typeArguments.length === 0) {
// this shouldn't happen in reality as:
// - tuples require at least 1 type argument
// - ReadonlyArray requires at least 1 type argument
return true;
}

// validate the element types are also readonly
if (typeArguments.some(typeArg => !isTypeReadonly(typeArg))) {
return false;
}
return true;
}

if (checker.isArrayType(type)) {
const symbol = util.nullThrows(
type.getSymbol(),
util.NullThrowsReasons.MissingToken('symbol', 'array type'),
);
const escapedName = symbol.getEscapedName();
if (escapedName === 'Array' && escapedName !== 'ReadonlyArray') {
return false;
}

return checkTypeArguments(type);
}

if (checker.isTupleType(type)) {
if (!type.target.readonly) {
return false;
}

return checkTypeArguments(type);
}

return null;
}

/**
* Returns:
* - null if the type is not an object,
* - true if the type is an object with only readonly props,
* - false if the type is an object with at least one mutable prop.
*/
function isTypeReadonlyObject(type: ts.Type): boolean | null {
function checkIndex(kind: ts.IndexKind): boolean | null {
const indexInfo = checker.getIndexInfoOfType(type, kind);
if (indexInfo) {
return indexInfo.isReadonly ? true : false;
}

return null;
}

const isStringIndexReadonly = checkIndex(ts.IndexKind.String);
if (isStringIndexReadonly !== null) {
return isStringIndexReadonly;
}

const isNumberIndexReadonly = checkIndex(ts.IndexKind.Number);
if (isNumberIndexReadonly !== null) {
return isNumberIndexReadonly;
}

const properties = type.getProperties();
if (properties.length) {
// NOTES:
// - port isReadonlySymbol - https://github.com/Microsoft/TypeScript/blob/4212484ae18163df867f53dab19a8cc0c6000793/src/compiler/checker.ts#L26285
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
declare function isReadonlySymbol(symbol: ts.Symbol): boolean;

for (const property of properties) {
if (!isReadonlySymbol(property)) {
return false;
}
}

// all properties were readonly
return true;
}

return false;
}

/**
* Checks if the given type is readonly
*/
function isTypeReadonly(type: ts.Type): boolean {
if (isUnionType(type)) {
return unionTypeParts(type).every(t => isTypeReadonly(t));
}

// all non-object types are readonly
if (!isObjectType(type)) {
return true;
}

// pure function types are readonly
if (
type.getCallSignatures().length > 0 &&
type.getProperties().length === 0
) {
return true;
}

const isReadonlyArray = isTypeReadonlyArrayOrTuple(type);
if (isReadonlyArray !== null) {
return isReadonlyArray;
}

const isReadonlyObject = isTypeReadonlyObject(type);
if (isReadonlyObject !== null) {
return isReadonlyObject;
}

throw new Error('Unhandled type');
}

return {
'ArrowFunctionExpression, FunctionDeclaration, FunctionExpression, TSEmptyBodyFunctionExpression'(
node:
Expand All @@ -174,7 +44,7 @@ export default util.createRule({
: param;
const tsNode = esTreeNodeToTSNodeMap.get(actualParam);
const type = checker.getTypeAtLocation(tsNode);
const isReadOnly = isTypeReadonly(type);
const isReadOnly = util.isTypeReadonly(checker, type);

if (!isReadOnly) {
return context.report({
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/util/index.ts
Expand Up @@ -2,6 +2,7 @@ import { ESLintUtils } from '@typescript-eslint/experimental-utils';

export * from './astUtils';
export * from './createRule';
export * from './isTypeReadonly';
export * from './misc';
export * from './nullThrows';
export * from './types';
Expand Down
147 changes: 147 additions & 0 deletions packages/eslint-plugin/src/util/isTypeReadonly/index.ts
@@ -0,0 +1,147 @@
import {
isObjectType,
isUnionType,
isUnionOrIntersectionType,
unionTypeParts,
} from 'tsutils';
import * as ts from 'typescript';
import { nullThrows, NullThrowsReasons } from '..';
import { isReadonlySymbol } from './isReadonlySymbol';

/**
* Returns:
* - null if the type is not an array or tuple,
* - true if the type is a readonly array or readonly tuple,
* - false if the type is a mutable array or mutable tuple.
*/
function isTypeReadonlyArrayOrTuple(
checker: ts.TypeChecker,
type: ts.Type,
): boolean | null {
function checkTypeArguments(arrayType: ts.TypeReference): boolean {
const typeArguments = checker.getTypeArguments(arrayType);
if (typeArguments.length === 0) {
// this shouldn't happen in reality as:
// - tuples require at least 1 type argument
// - ReadonlyArray requires at least 1 type argument
return true;
}

// validate the element types are also readonly
if (typeArguments.some(typeArg => !isTypeReadonly(checker, typeArg))) {
return false;
}
return true;
}

if (checker.isArrayType(type)) {
const symbol = nullThrows(
type.getSymbol(),
NullThrowsReasons.MissingToken('symbol', 'array type'),
);
const escapedName = symbol.getEscapedName();
if (escapedName === 'Array' && escapedName !== 'ReadonlyArray') {
return false;
}

return checkTypeArguments(type);
}

if (checker.isTupleType(type)) {
if (!type.target.readonly) {
return false;
}

return checkTypeArguments(type);
}

return null;
}

/**
* Returns:
* - null if the type is not an object,
* - true if the type is an object with only readonly props,
* - false if the type is an object with at least one mutable prop.
*/
function isTypeReadonlyObject(
checker: ts.TypeChecker,
type: ts.Type,
): boolean | null {
function checkIndexSignature(kind: ts.IndexKind): boolean | null {
const indexInfo = checker.getIndexInfoOfType(type, kind);
if (indexInfo) {
return indexInfo.isReadonly ? true : false;
}

return null;
}

const properties = type.getProperties();
if (properties.length) {
for (const property of properties) {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
const x = checker.isReadonlySymbol(property);
const y = isReadonlySymbol(property);
if (x !== y) {
throw new Error('FUCK');
}
if (!isReadonlySymbol(property)) {
return false;
}
}

// all properties were readonly
}

const isStringIndexSigReadonly = checkIndexSignature(ts.IndexKind.String);
if (isStringIndexSigReadonly === false) {
return isStringIndexSigReadonly;
}

const isNumberIndexSigReadonly = checkIndexSignature(ts.IndexKind.Number);
if (isNumberIndexSigReadonly === false) {
return isNumberIndexSigReadonly;
}

return true;
}

/**
* Checks if the given type is readonly
*/
function isTypeReadonly(checker: ts.TypeChecker, type: ts.Type): boolean {
if (isUnionType(type)) {
// all types in the union must be readonly
return unionTypeParts(type).every(t => isTypeReadonly(checker, t));
}

// all non-object, non-intersection types are readonly.
// this should only be primitive types
if (!isObjectType(type) && !isUnionOrIntersectionType(type)) {
return true;
}

// pure function types are readonly
if (
type.getCallSignatures().length > 0 &&
type.getProperties().length === 0
) {
return true;
}

const isReadonlyArray = isTypeReadonlyArrayOrTuple(checker, type);
if (isReadonlyArray !== null) {
return isReadonlyArray;
}

const isReadonlyObject = isTypeReadonlyObject(checker, type);
if (isReadonlyObject !== null) {
return isReadonlyObject;
}

throw new Error('Unhandled type');
}

export { isTypeReadonly };
82 changes: 82 additions & 0 deletions packages/eslint-plugin/src/util/isTypeReadonly/isReadonlySymbol.ts
@@ -0,0 +1,82 @@
// - this code is ported from typescript's type checker
// Starting at https://github.com/Microsoft/TypeScript/blob/4212484ae18163df867f53dab19a8cc0c6000793/src/compiler/checker.ts#L26285

import * as ts from 'typescript';

// #region internal types used for isReadonlySymbol

// we can't use module augmentation because typescript uses export = ts
/* eslint-disable @typescript-eslint/ban-ts-ignore */

// CheckFlags is actually const enum
// https://github.com/Microsoft/TypeScript/blob/236012e47b26fee210caa9cbd2e072ef9e99f9ae/src/compiler/types.ts#L4038
const enum CheckFlags {
Readonly = 1 << 3,
}
type GetCheckFlags = (symbol: ts.Symbol) => CheckFlags;
// @ts-ignore
const getCheckFlags: GetCheckFlags = ts.getCheckFlags;

type GetDeclarationModifierFlagsFromSymbol = (s: ts.Symbol) => ts.ModifierFlags;
const getDeclarationModifierFlagsFromSymbol: GetDeclarationModifierFlagsFromSymbol =
// @ts-ignore
ts.getDeclarationModifierFlagsFromSymbol;

/* eslint-enable @typescript-eslint/ban-ts-ignore */

// function getDeclarationNodeFlagsFromSymbol(s: ts.Symbol): ts.NodeFlags {
// return s.valueDeclaration ? ts.getCombinedNodeFlags(s.valueDeclaration) : 0;
// }

// #endregion

function isReadonlySymbol(symbol: ts.Symbol): boolean {
// The following symbols are considered read-only:
// Properties with a 'readonly' modifier
// Variables declared with 'const'
// Get accessors without matching set accessors
// Enum members
// Unions and intersections of the above (unions and intersections eagerly set isReadonly on creation)

// transient readonly property
if (getCheckFlags(symbol) & CheckFlags.Readonly) {
console.log('check flags is truthy');
return true;
}

// Properties with a 'readonly' modifier
if (
symbol.flags & ts.SymbolFlags.Property &&
getDeclarationModifierFlagsFromSymbol(symbol) & ts.ModifierFlags.Readonly
) {
return true;
}

// Variables declared with 'const'
// if (
// symbol.flags & ts.SymbolFlags.Variable &&
// getDeclarationNodeFlagsFromSymbol(symbol) & ts.NodeFlags.Const
// ) {
// return true;
// }

// Get accessors without matching set accessors
if (
symbol.flags & ts.SymbolFlags.Accessor &&
!(symbol.flags & ts.SymbolFlags.SetAccessor)
) {
return true;
}

// Enum members
if (symbol.flags & ts.SymbolFlags.EnumMember) {
return true;
}

return false;

// TODO - maybe add this check?
// || symbol.declarations.some(isReadonlyAssignmentDeclaration)
}

export { isReadonlySymbol };

0 comments on commit cde59a5

Please sign in to comment.