1// convert requires above to imports 2import { execSync } from 'child_process'; 3import fsNode from 'fs'; 4import { globSync } from 'glob'; 5import XML from 'xml-js'; 6import YAML from 'yaml'; 7 8import { 9 Closure, 10 CursorInfoOutput, 11 FileType, 12 FullyAnnotatedDecl, 13 OutputModuleDefinition, 14 OutputViewDefinition, 15 Structure, 16} from './types'; 17 18const rootDir = process.cwd(); 19const pattern = `${rootDir}/**/*.swift`; 20 21function getStructureFromFile(file: FileType) { 22 const command = 'sourcekitten structure --file ' + file.path; 23 24 try { 25 const output = execSync(command); 26 return JSON.parse(output.toString()); 27 } catch (error) { 28 console.error('An error occurred while executing the command:', error); 29 } 30} 31// find an object with "key.typename" : "ModuleDefinition" somewhere in the structure and return it 32function findModuleDefinitionInStructure(structure: Structure): Structure[] | null { 33 if (!structure) { 34 return null; 35 } 36 if (structure?.['key.typename'] === 'ModuleDefinition') { 37 const root = structure?.['key.substructure']; 38 if (!root) { 39 console.warn('Found ModuleDefinition but it is malformed'); 40 } 41 return root; 42 } 43 const substructure = structure['key.substructure']; 44 if (Array.isArray(substructure) && substructure.length > 0) { 45 for (const child of substructure) { 46 let result = null; 47 result = findModuleDefinitionInStructure(child); 48 if (result) { 49 return result; 50 } 51 } 52 } 53 return null; 54} 55 56// Read string straight from file – needed since we can't get cursorinfo for modulename 57function getIdentifierFromOffsetObject(offsetObject: Structure, file: FileType) { 58 // adding 1 and removing 1 to get rid of quotes 59 return file.content 60 .substring(offsetObject['key.offset'], offsetObject['key.offset'] + offsetObject['key.length']) 61 .replaceAll('"', ''); 62} 63 64function maybeUnwrapXMLStructs(type: string | Partial<{ _text: string; 'ref.struct': string }>) { 65 if (!type) { 66 return type; 67 } 68 if (typeof type === 'string') { 69 return type; 70 } 71 if (type['_text']) { 72 return type['_text']; 73 } 74 if (type['ref.struct']) { 75 return maybeUnwrapXMLStructs(type['ref.struct']); 76 } 77 return type; 78} 79 80function maybeWrapArray<T>(itemOrItems: T[] | T | null) { 81 if (!itemOrItems) { 82 return null; 83 } 84 if (Array.isArray(itemOrItems)) { 85 return itemOrItems; 86 } else { 87 return [itemOrItems]; 88 } 89} 90 91function parseXMLAnnotatedDeclarations(cursorInfoOutput: CursorInfoOutput) { 92 const xml = cursorInfoOutput['key.fully_annotated_decl']; 93 if (!xml) { 94 return null; 95 } 96 const parsed = XML.xml2js(xml, { compact: true }) as FullyAnnotatedDecl; 97 98 const parameters = 99 maybeWrapArray(parsed?.['decl.function.free']?.['decl.var.parameter'])?.map((p) => ({ 100 name: maybeUnwrapXMLStructs(p['decl.var.parameter.argument_label']), 101 typename: maybeUnwrapXMLStructs(p['decl.var.parameter.type']), 102 })) ?? []; 103 const returnType = maybeUnwrapXMLStructs( 104 parsed?.['decl.function.free']?.['decl.function.returntype'] 105 ); 106 return { parameters, returnType }; 107} 108 109let cachedSDKPath: string | null = null; 110function getSDKPath() { 111 if (cachedSDKPath) { 112 return cachedSDKPath; 113 } 114 const sdkPath = execSync('xcrun --sdk iphoneos --show-sdk-path').toString().trim(); 115 cachedSDKPath = sdkPath; 116 return cachedSDKPath; 117} 118 119// Read type description with sourcekitten, works only for variables 120function getTypeFromOffsetObject(offsetObject: Structure, file: FileType) { 121 if (!offsetObject) { 122 return null; 123 } 124 const request = { 125 'key.request': 'source.request.cursorinfo', 126 'key.sourcefile': file.path, 127 'key.offset': offsetObject['key.offset'], 128 'key.compilerargs': [file.path, '-target', 'arm64-apple-ios', '-sdk', getSDKPath()], 129 }; 130 const yamlRequest = YAML.stringify(request, { 131 defaultStringType: 'QUOTE_DOUBLE', 132 lineWidth: 0, 133 defaultKeyType: 'PLAIN', 134 // needed since behaviour of sourcekitten is not consistent 135 } as any).replace('"source.request.cursorinfo"', 'source.request.cursorinfo'); 136 137 const command = 'sourcekitten request --yaml "' + yamlRequest.replaceAll('"', '\\"') + '"'; 138 try { 139 const output = execSync(command, { stdio: 'pipe' }); 140 return parseXMLAnnotatedDeclarations(JSON.parse(output.toString())); 141 } catch (error) { 142 console.error('An error occurred while executing the command:', error); 143 } 144 return null; 145} 146 147function hasSubstructure(structureObject: Structure) { 148 return structureObject?.['key.substructure'] && structureObject['key.substructure'].length > 0; 149} 150 151function parseClosureTypes(structureObject: Structure) { 152 const closure = structureObject['key.substructure']?.find( 153 (s) => s['key.kind'] === 'source.lang.swift.expr.closure' 154 ); 155 if (!closure) { 156 return null; 157 } 158 const parameters = closure['key.substructure'] 159 ?.filter((s) => s['key.kind'] === 'source.lang.swift.decl.var.parameter') 160 .map((p) => ({ name: p['key.name'], typename: p['key.typename'] })); 161 162 // TODO: Figure out if possible 163 const returnType = 'unknown'; 164 return { parameters, returnType }; 165} 166 167// Used for functions,async functions, all of shape Identifier(name, closure or function) 168function findNamedDefinitionsOfType(type: string, moduleDefinition: Structure[], file: FileType) { 169 const definitionsOfType = moduleDefinition.filter((md) => md['key.name'] === type); 170 return definitionsOfType.map((d) => { 171 const definitionParams = d['key.substructure']; 172 const name = getIdentifierFromOffsetObject(definitionParams[0], file); 173 let types = null; 174 if (hasSubstructure(definitionParams[1])) { 175 types = parseClosureTypes(definitionParams[1]); 176 } else { 177 types = getTypeFromOffsetObject(definitionParams[1], file); 178 } 179 return { name, types }; 180 }); 181} 182 183// Used for events 184function findGroupedDefinitionsOfType(type: string, moduleDefinition: Structure[], file: FileType) { 185 const definitionsOfType = moduleDefinition.filter((md) => md['key.name'] === type); 186 return definitionsOfType.flatMap((d) => { 187 const definitionParams = d['key.substructure']; 188 return definitionParams.map((d) => ({ name: getIdentifierFromOffsetObject(d, file) })); 189 }); 190} 191 192function findAndParseView( 193 moduleDefinition: Structure[], 194 file: FileType 195): null | OutputViewDefinition { 196 const viewDefinition = moduleDefinition.find((md) => md['key.name'] === 'View'); 197 if (!viewDefinition) { 198 return null; 199 } 200 // we support reading view definitions from closure only 201 const viewModuleDefinition = 202 viewDefinition['key.substructure']?.[1]?.['key.substructure']?.[0]?.['key.substructure']?.[0]?.[ 203 'key.substructure' 204 ]; 205 if (!viewModuleDefinition) { 206 console.warn('Could not parse view definition'); 207 return null; 208 } 209 // let's drop nested view field (is null anyways) 210 const { view: _, ...definition } = parseModuleDefinition(viewModuleDefinition, file); 211 return definition; 212} 213 214function omitViewFromClosureArguments(definitions: Closure[]) { 215 return definitions.map((d) => ({ 216 ...d, 217 types: { 218 ...d.types, 219 parameters: d.types?.parameters?.filter((t, idx) => idx !== 0 && t.name !== 'view'), 220 }, 221 })); 222} 223 224function parseModuleDefinition( 225 moduleDefinition: Structure[], 226 file: FileType 227): OutputModuleDefinition { 228 const parsedDefinition = { 229 name: findNamedDefinitionsOfType('Name', moduleDefinition, file)?.[0]?.name, 230 functions: findNamedDefinitionsOfType('Function', moduleDefinition, file), 231 asyncFunctions: findNamedDefinitionsOfType('AsyncFunction', moduleDefinition, file), 232 events: findGroupedDefinitionsOfType('Events', moduleDefinition, file), 233 properties: findNamedDefinitionsOfType('Property', moduleDefinition, file), 234 props: omitViewFromClosureArguments(findNamedDefinitionsOfType('Prop', moduleDefinition, file)), 235 view: findAndParseView(moduleDefinition, file), 236 }; 237 return parsedDefinition; 238} 239 240function findModuleDefinitionsInFiles(files: string[]) { 241 const modules = []; 242 for (const path of files) { 243 const file = { path, content: fsNode.readFileSync(path, 'utf8') }; 244 const definition = findModuleDefinitionInStructure(getStructureFromFile(file)); 245 if (definition) { 246 modules.push(parseModuleDefinition(definition, file)); 247 } 248 } 249 return modules; 250} 251 252export function getAllExpoModulesInWorkingDirectory() { 253 const files = globSync(pattern); 254 return findModuleDefinitionsInFiles(files); 255} 256