183 lines
5.5 KiB
TypeScript
183 lines
5.5 KiB
TypeScript
|
import { TlArgument, TlEntry, TlFullSchema } from './types'
|
||
|
import { computeConstructorIdFromEntry } from './ctor-id'
|
||
|
|
||
|
export function mergeTlEntries(entries: TlEntry[]): TlEntry | string {
|
||
|
const first = entries[0]
|
||
|
|
||
|
const result: TlEntry = {
|
||
|
kind: first.kind,
|
||
|
name: first.name,
|
||
|
type: first.type,
|
||
|
id: first.id,
|
||
|
comment: first.comment,
|
||
|
generics: first.generics,
|
||
|
arguments: first.arguments,
|
||
|
}
|
||
|
|
||
|
// even if the entry contains id, let's re-calculate it just to be sure
|
||
|
result.id = computeConstructorIdFromEntry(result)
|
||
|
|
||
|
const argsIndex: Record<string, TlArgument> = {}
|
||
|
let lastTrueFlagIdx = -1
|
||
|
|
||
|
result.arguments.forEach((arg, idx) => {
|
||
|
argsIndex[arg.name] = arg
|
||
|
|
||
|
if (arg.predicate && arg.type === 'true') lastTrueFlagIdx = idx
|
||
|
})
|
||
|
|
||
|
for (let i = 1; i < entries.length; i++) {
|
||
|
const entry = entries[i]
|
||
|
|
||
|
// the only thing we should actually merge are optional true flags
|
||
|
// anything other than that is *not compatible* and we must return null
|
||
|
// (and also comments fwiw)
|
||
|
|
||
|
// even if the entry contains id, let's re-calculate it just to be sure
|
||
|
const ctorId = computeConstructorIdFromEntry(entry)
|
||
|
|
||
|
// check if basic fields match
|
||
|
if (
|
||
|
result.kind !== entry.kind ||
|
||
|
result.name !== entry.name ||
|
||
|
result.type !== entry.type ||
|
||
|
result.id !== ctorId
|
||
|
)
|
||
|
return 'basic info mismatch'
|
||
|
|
||
|
// since we re-calculated id manually, we can skip checking
|
||
|
// generics and arguments, and get straight to merging
|
||
|
|
||
|
if (!result.comment && entry.comment) {
|
||
|
result.comment = entry.comment
|
||
|
}
|
||
|
|
||
|
for (let j = 0; j < entry.arguments.length; j++) {
|
||
|
const entryArgument = entry.arguments[j]
|
||
|
const resultArgument = argsIndex[entryArgument.name]
|
||
|
|
||
|
if (!resultArgument) {
|
||
|
// yay a new arg
|
||
|
// we can only add optional true args, since any others will change id
|
||
|
// ids match, so this must be the case
|
||
|
|
||
|
result.arguments.splice(lastTrueFlagIdx += 1, 0, entryArgument)
|
||
|
argsIndex[entryArgument.name] = entryArgument
|
||
|
}
|
||
|
|
||
|
// args exists both in result and current entry
|
||
|
// since ctor ids match, it must be the same, so we don't need to check
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
export async function mergeTlSchemas(
|
||
|
schemas: TlFullSchema[],
|
||
|
onConflict: (
|
||
|
options: (TlEntry | undefined)[],
|
||
|
reason: string
|
||
|
) => TlEntry | undefined | Promise<TlEntry | undefined>
|
||
|
): Promise<TlFullSchema> {
|
||
|
const result: TlFullSchema = {
|
||
|
entries: [],
|
||
|
classes: {},
|
||
|
methods: {},
|
||
|
unions: {},
|
||
|
}
|
||
|
|
||
|
const resolvedConflictsClasses: Record<string, 1> = {}
|
||
|
const resolvedConflictsMethods: Record<string, 1> = {}
|
||
|
|
||
|
for (let i = 0; i < schemas.length; i++) {
|
||
|
const schema = schemas[i]
|
||
|
|
||
|
for (let i1 = 0; i1 < schema.entries.length; i1++) {
|
||
|
const entry = schema.entries[i1]
|
||
|
|
||
|
const kind = entry.kind === 'class' ? 'classes' : 'methods'
|
||
|
const index = result[kind]
|
||
|
const conflictIndex =
|
||
|
entry.kind === 'class'
|
||
|
? resolvedConflictsClasses
|
||
|
: resolvedConflictsMethods
|
||
|
|
||
|
if (entry.name in conflictIndex) {
|
||
|
// this entry was manually processed by user after a conflict
|
||
|
// and should be skipped
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if (!(entry.name in index)) {
|
||
|
// new one, add as-is
|
||
|
index[entry.name] = entry
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
const existing = index[entry.name]
|
||
|
const merged = mergeTlEntries([existing, entry])
|
||
|
|
||
|
if (typeof merged === 'string') {
|
||
|
// merge conflict
|
||
|
// find all candidates from all schemas and let the user decide
|
||
|
const candidates = schemas.map(
|
||
|
(schema) => schema[kind][entry.name]
|
||
|
)
|
||
|
|
||
|
const chosen = await onConflict(candidates, merged)
|
||
|
|
||
|
if (chosen) {
|
||
|
index[entry.name] = chosen
|
||
|
} else {
|
||
|
delete index[entry.name]
|
||
|
}
|
||
|
|
||
|
conflictIndex[entry.name] = 1
|
||
|
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
index[entry.name] = merged
|
||
|
}
|
||
|
|
||
|
for (const name in schema.unions) {
|
||
|
if (!schema.unions.hasOwnProperty(name)) continue
|
||
|
|
||
|
const union = schema.unions[name]
|
||
|
|
||
|
if (!(name in result.unions)) {
|
||
|
result.unions[name] = {
|
||
|
name,
|
||
|
classes: [],
|
||
|
}
|
||
|
}
|
||
|
const existing = result.unions[name]
|
||
|
|
||
|
if (union.comment && !existing.comment) {
|
||
|
existing.comment = union.comment
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// for simplicity sake, entries and unions are generated after merging is done
|
||
|
for (let i = 0; i < 2; i++) {
|
||
|
const kind = i === 0 ? 'classes' : 'methods'
|
||
|
const index = result[kind]
|
||
|
|
||
|
for (const name in index) {
|
||
|
if (!index.hasOwnProperty(name)) continue
|
||
|
|
||
|
const entry = index[name]
|
||
|
|
||
|
result.entries.push(entry)
|
||
|
|
||
|
if (kind === 'classes') {
|
||
|
result.unions[entry.type].classes.push(entry)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result
|
||
|
}
|