1 | import { tsUtils } from '@neo-one/ts-utils';
|
2 | import { utils } from '@neo-one/utils';
|
3 | import ts from 'typescript';
|
4 | import { Context } from './Context';
|
5 | import { createContextForDir } from './createContext';
|
6 | import { CircularLinkedDependencyError, MultipleContractsInFileError } from './errors';
|
7 |
|
8 | export interface ContractDependency {
|
9 | readonly filePath: string;
|
10 | readonly name: string;
|
11 | }
|
12 | export interface Contract {
|
13 | readonly filePath: string;
|
14 | readonly name: string;
|
15 | readonly dependencies: ReadonlyArray<ContractDependency>;
|
16 | }
|
17 | export type Contracts = ReadonlyArray<Contract>;
|
18 |
|
19 | interface FilePathToContract {
|
20 | readonly [filePath: string]: Contract;
|
21 | }
|
22 | interface FilePathToDependencies {
|
23 | readonly [filePath: string]: ReadonlyArray<ContractDependency>;
|
24 | }
|
25 |
|
26 | export 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 |
|
95 | const 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 |
|
107 | while (satisfied.length > 0) {
|
108 |
|
109 | const node = satisfied.shift();
|
110 | if (node === undefined) {
|
111 |
|
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 |
|
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 |
|
139 | export const scan = async (dir: string): Promise<Contracts> => {
|
140 | const context = await createContextForDir(dir);
|
141 |
|
142 | return scanContext(context);
|
143 | };
|