diff --git a/packages/analyzer/src/fixtures/prop-slots.tsx b/packages/analyzer/src/fixtures/prop-slots.tsx new file mode 100644 index 000000000..4f27d1903 --- /dev/null +++ b/packages/analyzer/src/fixtures/prop-slots.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; + +export interface PropsWithSlots { + reactNode: React.ReactNode; + reactNodeArray: React.ReactNode[]; + explicitReactNodeArray: React.ReactNodeArray; + reactChild: React.ReactChild; + reactChildArray: React.ReactChild[]; + reactElement: React.ReactElement; + reactElementArray: React.ReactElement[]; + jsxElement: JSX.Element; + jsxElementArray: JSX.Element[]; + union: React.ReactChild | React.ReactElement | JSX.Element | string; + unionArray: (React.ReactChild | React.ReactElement | JSX.Element | string)[]; + disjunct: string | any; + disjunctArray: string[]; +} + +export const ReactElement: React.SFC = () => null; diff --git a/packages/analyzer/src/react-utils/is-react-slot-type.test.ts b/packages/analyzer/src/react-utils/is-react-slot-type.test.ts new file mode 100644 index 000000000..63eb59b9b --- /dev/null +++ b/packages/analyzer/src/react-utils/is-react-slot-type.test.ts @@ -0,0 +1,75 @@ +import * as TestUtils from '../test-utils'; +import { isReactSlotType } from './is-react-slot-type'; + +const fixtures = require('fixturez')(__dirname); + +let ctx: ReturnType; + +beforeAll(() => { + ctx = TestUtils.getFixtureSourceFile('prop-slots.tsx', { fixtures }); +}); + +test('returns true for React.ReactNode', () => { + const prop = TestUtils.getNamedPropType('reactNode', ctx); + expect(isReactSlotType(prop.type, ctx)).toBe(true); +}); + +test('returns true for React.ReactNodeArray', () => { + const prop = TestUtils.getNamedPropType('explicitReactNodeArray', ctx); + expect(isReactSlotType(prop.type, ctx)).toBe(true); +}); + +test('returns true for React.ReactChild', () => { + const prop = TestUtils.getNamedPropType('reactChild', ctx); + expect(isReactSlotType(prop.type, ctx)).toBe(true); +}); + +test('returns true for React.ReactElement', () => { + const prop = TestUtils.getNamedPropType('reactElement', ctx); + expect(isReactSlotType(prop.type, ctx)).toBe(true); +}); + +test('returns true for JSX.Element', () => { + const prop = TestUtils.getNamedPropType('jsxElement', ctx); + expect(isReactSlotType(prop.type, ctx)).toBe(true); +}); + +test('returns true for union type with slot members', () => { + const prop = TestUtils.getNamedPropType('union', ctx); + expect(isReactSlotType(prop.type, ctx)).toBe(true); +}); + +test('returns true for union type with only disjunct members', () => { + const prop = TestUtils.getNamedPropType('disjunct', ctx); + expect(isReactSlotType(prop.type, ctx)).toBe(false); +}); + +test('returns true for React.ReactNode[]', () => { + const prop = TestUtils.getNamedPropType('reactNodeArray', ctx); + expect(isReactSlotType(prop.type, ctx)).toBe(true); +}); + +test('returns true for React.ReactChild[]', () => { + const prop = TestUtils.getNamedPropType('reactChildArray', ctx); + expect(isReactSlotType(prop.type, ctx)).toBe(true); +}); + +test('returns true for React.ReactElement[]', () => { + const prop = TestUtils.getNamedPropType('reactElementArray', ctx); + expect(isReactSlotType(prop.type, ctx)).toBe(true); +}); + +test('returns true for JSX.Element[]', () => { + const prop = TestUtils.getNamedPropType('jsxElementArray', ctx); + expect(isReactSlotType(prop.type, ctx)).toBe(true); +}); + +test('returns true for Union[]', () => { + const prop = TestUtils.getNamedPropType('unionArray', ctx); + expect(isReactSlotType(prop.type, ctx)).toBe(true); +}); + +test('returns true for Disjunct[]', () => { + const prop = TestUtils.getNamedPropType('disjunctArray', ctx); + expect(isReactSlotType(prop.type, ctx)).toBe(false); +}); diff --git a/packages/analyzer/src/react-utils/is-react-slot-type.ts b/packages/analyzer/src/react-utils/is-react-slot-type.ts index ee982736f..5369fd6a4 100644 --- a/packages/analyzer/src/react-utils/is-react-slot-type.ts +++ b/packages/analyzer/src/react-utils/is-react-slot-type.ts @@ -1,13 +1,19 @@ // tslint:disable:no-bitwise import { hasReactTypings } from './has-react-typings'; import * as TypeScript from 'typescript'; +import { isUnionType } from '../typescript-utils/is-union-type'; +import { isTypeReference } from '../typescript-utils/is-type-reference'; -const REACT_SLOT_TYPES = ['Element', 'ReactNode', 'ReactChild']; +const REACT_SLOT_TYPES = ['Element', 'ReactElement', 'ReactNode', 'ReactNodeArray', 'ReactChild']; export function isReactSlotType( type: TypeScript.Type, ctx: { program: TypeScript.Program } ): boolean { + if (isUnionType(type)) { + return type.types.some(typeMember => isReactSlotType(typeMember, ctx)); + } + const typechecker = ctx.program.getTypeChecker(); const symbol = type.aliasSymbol || type.symbol || type.getSymbol(); @@ -20,6 +26,16 @@ export function isReactSlotType( ? typechecker.getAliasedSymbol(symbol) : symbol; + if (resolvedSymbol.name === 'Array' && isTypeReference(type) && type.typeArguments) { + const arg = type.typeArguments[0]!; + + if (!arg) { + return false; + } + + return isReactSlotType(arg, ctx); + } + if (!REACT_SLOT_TYPES.includes(resolvedSymbol.name)) { return false; } diff --git a/packages/analyzer/src/test-utils.ts b/packages/analyzer/src/test-utils.ts index 93997cf80..305cbd228 100644 --- a/packages/analyzer/src/test-utils.ts +++ b/packages/analyzer/src/test-utils.ts @@ -16,14 +16,16 @@ export interface Export { type: TypeScript.Type; } -export const getFixtureSourceFile = ( - name: string | string[], - ctx: { fixtures: Fixtures } -): { +export interface FixtureSourceFile { sourceFile: TypeScript.SourceFile; sourceFiles: TypeScript.SourceFile[]; program: TypeScript.Program; -} => { +} + +export const getFixtureSourceFile = ( + name: string | string[], + ctx: { fixtures: Fixtures } +): FixtureSourceFile => { const names = Array.isArray(name) ? name : [name]; const paths = names .map(n => ctx.fixtures.find(n)) @@ -61,6 +63,22 @@ export const getFirstPropType = ( return propTypes[0]; }; +export const getNamedPropType = (name: string, ctx: FixtureSourceFile): Prop => { + const props = getPropTypes(ctx.sourceFile, ctx); + + const result = props.find(prop => prop.symbol.getName() === name); + + if (!result) { + throw new Error( + `Could not find prop with name ${name}. Available props: ${props + .map(p => p.symbol.getName()) + .join(', ')}` + ); + } + + return result; +}; + export const getPropTypes = ( sourceFile: TypeScript.SourceFile, ctx: { program: TypeScript.Program } diff --git a/packages/analyzer/src/typescript-utils/is-type-reference.ts b/packages/analyzer/src/typescript-utils/is-type-reference.ts new file mode 100644 index 000000000..27e11616d --- /dev/null +++ b/packages/analyzer/src/typescript-utils/is-type-reference.ts @@ -0,0 +1,15 @@ +import * as TypeScript from 'typescript'; + +export function isTypeReference(t: TypeScript.Type): t is TypeScript.TypeReference { + if (!isObjectType(t)) { + return false; + } + + // tslint:disable-next-line:no-bitwise + return (t.objectFlags & TypeScript.ObjectFlags.Reference) === TypeScript.ObjectFlags.Reference; +} + +function isObjectType(t: TypeScript.Type): t is TypeScript.ObjectType { + // tslint:disable-next-line:no-bitwise + return (t.flags & TypeScript.TypeFlags.Object) === TypeScript.TypeFlags.Object; +} diff --git a/packages/analyzer/src/typescript-utils/is-union-type.ts b/packages/analyzer/src/typescript-utils/is-union-type.ts new file mode 100644 index 000000000..7de3138e3 --- /dev/null +++ b/packages/analyzer/src/typescript-utils/is-union-type.ts @@ -0,0 +1,6 @@ +import * as TypeScript from 'typescript'; + +export function isUnionType(t: TypeScript.Type): t is TypeScript.UnionType { + // tslint:disable-next-line:no-bitwise + return (t.flags & TypeScript.TypeFlags.Union) === TypeScript.TypeFlags.Union; +}