Skip to content
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
38 changes: 32 additions & 6 deletions packages/eslint-plugin-specs/react-native-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@ const ERRORS = {
let RNModuleParser;
let RNParserUtils;
let RNFlowParser;
let RNParserCommons;
let RNFlowParserUtils;

function requireModuleParser() {
if (RNModuleParser == null || RNParserUtils == null || RNFlowParser == null) {
if (
RNModuleParser == null ||
RNParserUtils == null ||
RNFlowParser == null ||
RNParserCommons == null ||
RNFlowParserUtils == null
) {
// If using this externally, we leverage @react-native/codegen as published form
if (!PACKAGE_USAGE) {
const config = {
Expand All @@ -38,6 +46,8 @@ function requireModuleParser() {
RNModuleParser = require('@react-native/codegen/src/parsers/flow/modules');
RNParserUtils = require('@react-native/codegen/src/parsers/utils');
RNFlowParser = require('@react-native/codegen/src/parsers/flow/parser');
RNParserCommons = require('@react-native/codegen/src/parsers/parsers-commons');
RNFlowParserUtils = require('@react-native/codegen/src/parsers/flow/utils');
});
} else {
const config = {
Expand All @@ -47,16 +57,20 @@ function requireModuleParser() {

withBabelRegister(config, () => {
RNModuleParser = require('@react-native/codegen/lib/parsers/flow/modules');
RNParserUtils = require('@react-native/codegen/lib/parsers/flow/utils');
RNParserUtils = require('@react-native/codegen/lib/parsers/utils');
RNFlowParser = require('@react-native/codegen/lib/parsers/flow/parser');
RNParserCommons = require('@react-native/codegen/lib/parsers/parsers-commons');
RNFlowParserUtils = require('@react-native/codegen/lib/parsers/flow/utils');
});
}
}

return {
buildModuleSchema: RNModuleParser.buildModuleSchema,
buildModuleSchema: RNParserCommons.buildModuleSchema,
createParserErrorCapturer: RNParserUtils.createParserErrorCapturer,
parser: new RNFlowParser.FlowParser(),
resolveTypeAnnotation: RNFlowParserUtils.resolveTypeAnnotation,
translateTypeAnnotation: RNModuleParser.flowTranslateTypeAnnotation,
};
}

Expand Down Expand Up @@ -131,16 +145,28 @@ function rule(context) {
});
}

const {buildModuleSchema, createParserErrorCapturer, parser} =
requireModuleParser();
const {
buildModuleSchema,
createParserErrorCapturer,
parser,
resolveTypeAnnotation,
translateTypeAnnotation,
} = requireModuleParser();

const [parsingErrors, tryParse] = createParserErrorCapturer();

const sourceCode = context.getSourceCode().getText();
const ast = parser.getAst(sourceCode);

tryParse(() => {
buildModuleSchema(hasteModuleName, ast, tryParse, parser);
buildModuleSchema(
hasteModuleName,
ast,
tryParse,
parser,
resolveTypeAnnotation,
translateTypeAnnotation,
);
});

parsingErrors.forEach(error => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ import type {ParserType} from '../errors';
const {Visitor} = require('../flow/Visitor');
const {wrapComponentSchema} = require('../schema.js');
const {buildComponentSchema} = require('../flow/components');
const {buildModuleSchema} = require('../flow/modules');
const {isModuleRegistryCall} = require('../utils.js');
const {buildModuleSchema} = require('../parsers-commons.js');
const {
isModuleRegistryCall,
createParserErrorCapturer,
} = require('../utils.js');
const {
ParserError,
UnsupportedObjectPropertyTypeAnnotationParserError,
Expand All @@ -36,6 +39,9 @@ const {
IncorrectModuleRegistryCallArityParserError,
IncorrectModuleRegistryCallArgumentTypeParserError,
UntypedModuleRegistryCallParserError,
ModuleInterfaceNotFoundParserError,
MoreThanOneModuleInterfaceParserError,
MisnamedModuleInterfaceParserError,
} = require('../errors');

import {MockedParser} from '../parserMock';
Expand All @@ -45,8 +51,9 @@ const parser = new MockedParser();

const flowParser = new FlowParser();

const flowTranslateTypeAnnotation = require('../flow/modules/index');
const {flowTranslateTypeAnnotation} = require('../flow/modules/index');
const typeScriptTranslateTypeAnnotation = require('../typescript/modules/index');
const {resolveTypeAnnotation} = require('../flow/utils');

beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -396,7 +403,9 @@ describe('buildSchemaFromConfigType', () => {
const buildComponentSchemaMock = jest.fn(
(_ast, _parser) => componentSchemaMock,
);
const buildModuleSchemaMock = jest.fn((_0, _1, _2, _3) => moduleSchemaMock);
const buildModuleSchemaMock = jest.fn(
(_0, _1, _2, _3, _4, _5) => moduleSchemaMock,
);

const buildSchemaFromConfigTypeHelper = (
configType: 'module' | 'component' | 'none',
Expand All @@ -410,6 +419,8 @@ describe('buildSchemaFromConfigType', () => {
buildComponentSchemaMock,
buildModuleSchemaMock,
parser,
resolveTypeAnnotation,
flowTranslateTypeAnnotation,
);

describe('when configType is none', () => {
Expand Down Expand Up @@ -491,6 +502,8 @@ describe('buildSchemaFromConfigType', () => {
astMock,
expect.any(Function),
parser,
resolveTypeAnnotation,
flowTranslateTypeAnnotation,
);

expect(buildComponentSchemaMock).not.toHaveBeenCalled();
Expand Down Expand Up @@ -661,6 +674,8 @@ describe('buildSchema', () => {
buildModuleSchema,
Visitor,
parser,
resolveTypeAnnotation,
flowTranslateTypeAnnotation,
);

expect(getConfigTypeSpy).not.toHaveBeenCalled();
Expand Down Expand Up @@ -693,6 +708,8 @@ describe('buildSchema', () => {
buildModuleSchema,
Visitor,
flowParser,
resolveTypeAnnotation,
flowTranslateTypeAnnotation,
);

expect(getConfigTypeSpy).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -746,6 +763,8 @@ describe('buildSchema', () => {
buildModuleSchema,
Visitor,
flowParser,
resolveTypeAnnotation,
flowTranslateTypeAnnotation,
);

expect(getConfigTypeSpy).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -1007,3 +1026,208 @@ describe('parseModuleName', () => {
});
});
});

describe('buildModuleSchema', () => {
const hasteModuleName = 'TestModuleName';
const [, tryParse] = createParserErrorCapturer();
const language = flowParser.language();
const NATIVE_MODULE = `
import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport';
import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry';

export interface Spec extends TurboModule {
+getArray: (a: Array<any>) => Array<string>;
}

export default (TurboModuleRegistry.getEnforcing<Spec>(
'SampleTurboModule',
): Spec);
`;

describe('throwIfModuleInterfaceNotFound', () => {
it('should throw ModuleInterfaceNotFoundParserError if no module interface is found', () => {
const ast = flowParser.getAst('');
const expected = new ModuleInterfaceNotFoundParserError(
hasteModuleName,
ast,
language,
);

expect(() =>
buildModuleSchema(
hasteModuleName,
ast,
tryParse,
flowParser,
resolveTypeAnnotation,
flowTranslateTypeAnnotation,
),
).toThrow(expected);
});

it('should not throw ModuleInterfaceNotFoundParserError if module interface is found', () => {
const ast = flowParser.getAst(NATIVE_MODULE);

expect(() =>
buildModuleSchema(
hasteModuleName,
ast,
tryParse,
flowParser,
resolveTypeAnnotation,
flowTranslateTypeAnnotation,
),
).not.toThrow();
});
});

describe('throwIfMoreThanOneModuleInterfaceParser', () => {
it('should throw an error if mulitple module interfaces are found', () => {
const contents = `
import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport';
import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry';

export interface Spec extends TurboModule {
+getBool: (arg: boolean) => boolean; }
export interface SpecOther extends TurboModule {
+getArray: (a: Array<any>) => Array<string>;
}
`;
const ast = flowParser.getAst(contents);
const types = flowParser.getTypes(ast);
const moduleSpecs = Object.values(types).filter(t =>
flowParser.isModuleInterface(t),
);
const expected = new MoreThanOneModuleInterfaceParserError(
hasteModuleName,
moduleSpecs,
moduleSpecs.map(node => node.id.name),
language,
);

expect(() =>
buildModuleSchema(
hasteModuleName,
ast,
tryParse,
flowParser,
resolveTypeAnnotation,
flowTranslateTypeAnnotation,
),
).toThrow(expected);
});

it('should not throw an error if exactly one module interface is found', () => {
const ast = flowParser.getAst(NATIVE_MODULE);

expect(() =>
buildModuleSchema(
hasteModuleName,
ast,
tryParse,
flowParser,
resolveTypeAnnotation,
flowTranslateTypeAnnotation,
),
).not.toThrow();
});
});

describe('throwIfModuleInterfaceIsMisnamed', () => {
it('should throw an error if module interface is misnamed', () => {
const contents = `
import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport';
import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry';

export interface MisnamedSpec extends TurboModule {
+getArray: (a: Array<any>) => Array<string>;
}

export default (TurboModuleRegistry.getEnforcing<Spec>(
'SampleTurboModule',
): Spec);
`;
const ast = flowParser.getAst(contents);
const types = flowParser.getTypes(ast);
const moduleSpecs = Object.values(types).filter(t =>
flowParser.isModuleInterface(t),
);
const [moduleSpec] = moduleSpecs;

const expected = new MisnamedModuleInterfaceParserError(
hasteModuleName,
moduleSpec.id,
language,
);

expect(() =>
buildModuleSchema(
hasteModuleName,
ast,
tryParse,
flowParser,
resolveTypeAnnotation,
flowTranslateTypeAnnotation,
),
).toThrow(expected);
});

it('should not throw an error if module interface is correctly named', () => {
const ast = flowParser.getAst(NATIVE_MODULE);

expect(() =>
buildModuleSchema(
hasteModuleName,
ast,
tryParse,
flowParser,
resolveTypeAnnotation,
flowTranslateTypeAnnotation,
),
).not.toThrow();
});
});

it('should return valid module schema', () => {
const ast = flowParser.getAst(NATIVE_MODULE);
const schmeaMock = {
aliasMap: {},
enumMap: {},
excludedPlatforms: undefined,
moduleName: 'SampleTurboModule',
spec: {
properties: [
{
name: 'getArray',
optional: false,
typeAnnotation: {
params: [
{
name: 'a',
optional: false,
typeAnnotation: {type: 'ArrayTypeAnnotation'},
},
],
returnTypeAnnotation: {
elementType: {type: 'StringTypeAnnotation'},
type: 'ArrayTypeAnnotation',
},
type: 'FunctionTypeAnnotation',
},
},
],
},
type: 'NativeModule',
};
const schema = buildModuleSchema(
hasteModuleName,
ast,
tryParse,
flowParser,
resolveTypeAnnotation,
flowTranslateTypeAnnotation,
);

expect(schema).toEqual(schmeaMock);
});
});
Loading