UNPKG

4.68 kBPlain TextView Raw
1import { tsUtils } from '@neo-one/ts-utils';
2import { utils } from '@neo-one/utils';
3import ts from 'typescript';
4import { Context } from './Context';
5import { createContextForDir } from './createContext';
6import { CircularLinkedDependencyError, MultipleContractsInFileError } from './errors';
7
8export interface ContractDependency {
9 readonly filePath: string;
10 readonly name: string;
11}
12export interface Contract {
13 readonly filePath: string;
14 readonly name: string;
15 readonly dependencies: ReadonlyArray<ContractDependency>;
16}
17export type Contracts = ReadonlyArray<Contract>;
18
19interface FilePathToContract {
20 readonly [filePath: string]: Contract;
21}
22interface FilePathToDependencies {
23 readonly [filePath: string]: ReadonlyArray<ContractDependency>;
24}
25
26export const scanContext = (context: Context): Contracts => {
27 const smartContract = tsUtils.symbol.getDeclarations(context.builtins.getInterfaceSymbol('SmartContract'))[0];
28 if (!ts.isInterfaceDeclaration(smartContract)) {
29 throw new Error('Something went wrong!');
30 }
31
32 const { contracts, dependencies } = tsUtils.class_
33 .getImplementors(context.program, context.languageService, smartContract)
34 .reduce<{ contracts: FilePathToContract; dependencies: FilePathToDependencies }>(
35 (acc, derived) => {
36 if (!tsUtils.modifier.isAbstract(derived)) {
37 const filePath = tsUtils.file.getFilePath(tsUtils.node.getSourceFile(derived));
38 const name = tsUtils.node.getNameOrThrow(derived);
39 const existing = acc.contracts[filePath] as Contract | undefined;
40 if (existing !== undefined) {
41 throw new MultipleContractsInFileError(filePath);
42 }
43
44 const references = [
45 ...new Set(
46 tsUtils.reference
47 .findReferencesAsNodes(context.program, context.languageService, derived)
48 .map((reference) => tsUtils.file.getFilePath(tsUtils.node.getSourceFile(reference))),
49 ),
50 ];
51
52 const dependency = { filePath, name };
53 const dependenciesOut = references.reduce((innerAcc, reference) => {
54 let filePathDependencies = innerAcc[reference] as ReadonlyArray<ContractDependency> | undefined;
55 if (filePathDependencies === undefined) {
56 filePathDependencies = [];
57 }
58
59 return {
60 ...innerAcc,
61 [reference]: [...filePathDependencies, dependency],
62 };
63 }, acc.dependencies);
64
65 return {
66 contracts: {
67 ...acc.contracts,
68 [filePath]: {
69 filePath,
70 name,
71 dependencies: [],
72 },
73 },
74 dependencies: dependenciesOut,
75 };
76 }
77
78 return acc;
79 },
80 { contracts: {}, dependencies: {} },
81 );
82
83 const unsortedContracts = Object.values(contracts).map((contract) => {
84 const filePathDependencies = dependencies[contract.filePath] as ReadonlyArray<ContractDependency> | undefined;
85
86 return {
87 ...contract,
88 dependencies: filePathDependencies === undefined ? [] : filePathDependencies,
89 };
90 });
91
92 return topographicalSort(unsortedContracts);
93};
94
95const topographicalSort = (contracts: Contracts): Contracts => {
96 const contractToDependencies = contracts.reduce<{ [filePath: string]: Set<string> }>(
97 (acc, contract) => ({
98 ...acc,
99 [contract.filePath]: new Set(contract.dependencies.map((dep) => dep.filePath)),
100 }),
101 {},
102 );
103 const mutableOut: Contract[] = [];
104 const satisfied = contracts.filter((contract) => contract.dependencies.length === 0);
105 let remaining = contracts.filter((contract) => contract.dependencies.length !== 0);
106 // tslint:disable-next-line no-loop-statement
107 while (satisfied.length > 0) {
108 // tslint:disable-next-line no-array-mutation
109 const node = satisfied.shift();
110 if (node === undefined) {
111 /* istanbul ignore next */
112 break;
113 }
114
115 mutableOut.push(node);
116 remaining = remaining
117 .map((contract) => {
118 const deps = contractToDependencies[contract.filePath];
119 deps.delete(node.filePath);
120 if (deps.size === 0) {
121 // tslint:disable-next-line no-array-mutation
122 satisfied.push(contract);
123
124 return undefined;
125 }
126
127 return contract;
128 })
129 .filter(utils.notNull);
130 }
131
132 if (mutableOut.length !== contracts.length) {
133 throw new CircularLinkedDependencyError();
134 }
135
136 return mutableOut;
137};
138
139export const scan = async (dir: string): Promise<Contracts> => {
140 const context = await createContextForDir(dir);
141
142 return scanContext(context);
143};