<commit_before>"use strict";

import { includes, startsWith } from "lodash";
import * as pluralize from "pluralize";

import { TargetLanguage } from "../TargetLanguage";
import { Type, ClassType, EnumType, nullableFromUnion, matchType, ArrayType, MapType, UnionType } from "../Type";
import { TypeGraph } from "../TypeGraph";
import { Namespace, Name, Namer, funPrefixNamer } from "../Naming";
import { Sourcelike, modifySource } from "../Source";
import {
    splitIntoWords,
    combineWords,
    firstUpperWordStyle,
    allUpperWordStyle,
    allLowerWordStyle,
    camelCase,
    utf16LegalizeCharacters,
    stringEscape
} from "../Strings";
import { ConvenienceRenderer } from "../ConvenienceRenderer";
import { StringOption, BooleanOption, EnumOption } from "../RendererOptions";
import { Set } from "immutable";

const unicode = require("unicode-properties");

type OutputFeatures = { interface: boolean; implementation: boolean };

export default class ObjectiveCTargetLanguage extends TargetLanguage {
    private readonly _featuresOption = new EnumOption("features", "Interface and implementation", [
        ["all", { interface: true, implementation: true }],
        ["interface", { interface: true, implementation: false }],
        ["implementation", { interface: false, implementation: true }]
    ]);
    private readonly _justTypesOption = new BooleanOption("just-types", "Plain types only", false);
    private readonly _classPrefixOption = new StringOption("class-prefix", "Class prefix", "PREFIX", "QT");
    private readonly _extraCommentsOption = new BooleanOption("extra-comments", "Extra comments", false);

    constructor() {
        super("Objective-C", ["objc", "objective-c", "objectivec"], "m");
        this.setOptions([
            this._justTypesOption,
            this._classPrefixOption,
            this._featuresOption,
            this._extraCommentsOption
        ]);
    }

    protected get rendererClass(): new (
        graph: TypeGraph,
        leadingComments: string[] | undefined,
        ...optionValues: any[]
    ) => ConvenienceRenderer {
        return ObjectiveCRenderer;
    }
}

function typeNameStyle(prefix: string, original: string): string {
    const words = splitIntoWords(original);
    const result = combineWords(
        words,
        legalizeName,
        firstUpperWordStyle,
        firstUpperWordStyle,
        allUpperWordStyle,
        allUpperWordStyle,
        "",
        isStartCharacter
    );
    // Take care not to doubly-prefix type names
    return startsWith(result, prefix) ? result : prefix + result;
}

function propertyNameStyle(original: string): string {
    const words = splitIntoWords(original);
    return combineWords(
        words,
        legalizeName,
        allLowerWordStyle,
        firstUpperWordStyle,
        allLowerWordStyle,
        allUpperWordStyle,
        "",
        isStartCharacter
    );
}

const keywords = [
    "_Bool",
    "_Complex",
    "_Imaginary",
    "atomic",
    "auto",
    "break",
    "case ",
    "char",
    "const",
    "continue",
    "default",
    "do",
    "double",
    "else",
    "enum",
    "extern",
    "false",
    "float ",
    "for ",
    "goto",
    "if",
    "inline",
    "int",
    "long",
    "nil",
    "nonatomic",
    "register",
    "restrict",
    "retain",
    "return",
    "short",
    "signed",
    "sizeof",
    "static",
    "struct",
    "switch",
    "typedef",
    "true",
    "union",
    "unsigned",
    "void",
    "volatile",
    "while"
];

const propertySafeKeywords = [
    "BOOL",
    "Class",
    "bycopy",
    "byref",
    "decimal",
    "id",
    "IMP",
    "in",
    "inout",
    "NO",
    "NULL",
    "oneway",
    "out",
    "Protocol",
    "SEL",
    "self",
    "super",
    "YES"
];

function isStartCharacter(utf16Unit: number): boolean {
    return unicode.isAlphabetic(utf16Unit) || utf16Unit === 0x5f; // underscore
}

function isPartCharacter(utf16Unit: number): boolean {
    const category: string = unicode.getCategory(utf16Unit);
    return includes(["Nd", "Pc", "Mn", "Mc"], category) || isStartCharacter(utf16Unit);
}

const legalizeName = utf16LegalizeCharacters(isPartCharacter);

function isAnyOrNull(t: Type): boolean {
    return t.kind === "any" || t.kind === "null";
}

class ObjectiveCRenderer extends ConvenienceRenderer {
    // enums contained in NSArray or NSDictionary are represented
    // as 'pseudo-enum' reference types. Eventually we may want to
    // support 'natural' enums (NSUInteger) but this isn't implemented yet.
    pseudoEnums: Set<EnumType>;

    constructor(
        graph: TypeGraph,
        leadingComments: string[] | undefined,
        private readonly _justTypes: boolean,
        private readonly _classPrefix: string,
        private readonly _features: OutputFeatures,
        private readonly _extraComments: boolean
    ) {
        super(graph, leadingComments);

        this.pseudoEnums = graph
            .allTypesUnordered()
            .filter(t => t instanceof EnumType && this.enumOccursInArrayOrMap(t))
            .map(t => t as EnumType);
    }

    private enumOccursInArrayOrMap(enumType: EnumType): boolean {
        function containedBy(t: Type): boolean {
            return (
                (t instanceof ArrayType && t.items.equals(enumType)) ||
                (t instanceof MapType && t.values.equals(enumType)) ||
                (t instanceof ClassType && t.properties.some(containedBy)) ||
                // TODO support unions
                (t instanceof UnionType && false)
            );
        }
        return this.typeGraph.allTypesUnordered().some(containedBy);
    }

    protected get forbiddenNamesForGlobalNamespace(): string[] {
        return keywords;
    }

    protected forbiddenForClassProperties(
        _c: ClassType,
        _classNamed: Name
    ): { names: Name[]; namespaces: Namespace[] } {
        return { names: [], namespaces: [this.forbiddenWordsNamespace] };
    }

    protected forbiddenForEnumCases(_e: EnumType, _enumNamed: Name): { names: Name[]; namespaces: Namespace[] } {
        return { names: [], namespaces: [this.forbiddenWordsNamespace] };
    }

    protected topLevelNameStyle(rawName: string): string {
        return camelCase(rawName);
    }

    protected makeNamedTypeNamer(): Namer {
        return funPrefixNamer(rawName => typeNameStyle(this._classPrefix, rawName));
    }

    protected makeClassPropertyNamer(): Namer {
        // TODO why is underscore being removed?
        return new Namer(s => propertyNameStyle(s), ["_", "the", "one", "some", "another"]);
    }

    protected makeUnionMemberNamer(): null {
        return null;
    }

    protected makeEnumCaseNamer(): Namer {
        return new Namer(propertyNameStyle, []);
    }

    protected namedTypeToNameForTopLevel(type: Type): Type | undefined {
        return type;
    }

    private emitBlock = (line: Sourcelike, f: () => void): void => {
        this.emitLine(line, " {");
        this.indent(f);
        this.emitLine("}");
    };

    private emitMethod(declaration: Sourcelike, f: () => void) {
        this.emitLine(declaration);
        this.emitLine("{");
        this.indent(f);
        this.emitLine("}");
    }

    private emitExtraComments = (...comments: Sourcelike[]) => {
        if (!this._extraComments) return;
        for (const comment of comments) {
            this.emitLine("// ", comment);
        }
    };

    private objcType = (t: Type): [Sourcelike, string] => {
        return matchType<[Sourcelike, string]>(
            t,
            _anyType => ["id", ""],
            // For now, we're treating nulls just like any
            _nullType => ["id", ""],
            _boolType => ["NSBoolean", " *"],
            _integerType => ["NSNumber", " *"],
            _doubleType => ["NSNumber", " *"],
            _stringType => ["NSString", " *"],
            arrayType => {
                const itemType = arrayType.items;
                const itemTypeName = this.objcType(itemType);
                // NSArray<id>* === NSArray*
                if (isAnyOrNull(itemType)) {
                    return ["NSArray", " *"];
                }
                return [["NSArray<", itemTypeName, ">"], " *"];
            },
            classType => [this.nameForNamedType(classType), " *"],
            mapType => [["NSDictionary<NSString *, ", this.objcType(mapType.values), ">"], " *"],
            enumType => [this.nameForNamedType(enumType), " *"],
            unionType => {
                const nullable = nullableFromUnion(unionType);
                return nullable !== null ? this.objcType(nullable) : ["id", ""];
            }
        );
    };

    private jsonType = (t: Type): [Sourcelike, string] => {
        return matchType<[Sourcelike, string]>(
            t,
            _anyType => ["id", ""],
            // For now, we're treating nulls just like any
            _nullType => ["id", ""],
            _boolType => ["NSBoolean", " *"],
            _integerType => ["NSNumber", " *"],
            _doubleType => ["NSNumber", " *"],
            _stringType => ["NSString", " *"],
            _arrayType => ["NSArray", " *"],
            _classType => ["NSDictionary<NSString *, id>", " *"],
            mapType => [["NSDictionary<NSString *, ", this.jsonType(mapType.values), ">"], " *"],
            _enumType => ["NSString", " *"],
            unionType => {
                const nullable = nullableFromUnion(unionType);
                return nullable !== null ? this.jsonType(nullable) : ["id", ""];
            }
        );
    };

    private fromDynamicExpression = (t: Type, ...dynamic: Sourcelike[]): Sourcelike => {
        return matchType<Sourcelike>(
            t,
            _anyType => dynamic,
            _nullType => dynamic,
            _boolType => dynamic,
            _integerType => dynamic,
            _doubleType => dynamic,
            _stringType => dynamic,
            arrayType => ["[", dynamic, " map:λ(id x, ", this.fromDynamicExpression(arrayType.items, "x"), ")]"],
            classType => ["[", this.nameForNamedType(classType), " fromJSONDictionary:", dynamic, "]"],
            mapType => ["[", dynamic, " map:λ(id x, ", this.fromDynamicExpression(mapType.values, "x"), ")]"],
            enumType => ["NOT_NIL([", this.nameForNamedType(enumType), " withValue:", dynamic, "])"],
            unionType => {
                const nullable = nullableFromUnion(unionType);
                return nullable !== null ? this.fromDynamicExpression(nullable, dynamic) : dynamic;
            }
        );
    };

    private toDynamicExpression = (t: Type, dynamic: Sourcelike): Sourcelike => {
        return matchType<Sourcelike>(
            t,
            _anyType => ["NSNullify(", dynamic, ")"],
            _nullType => ["NSNullify(", dynamic, ")"],
            _boolType => dynamic,
            _integerType => dynamic,
            _doubleType => dynamic,
            _stringType => dynamic,
            arrayType => {
                if (this.isJSONSafe(arrayType)) {
                    // TODO check each value type
                    return dynamic;
                }
                return ["[", dynamic, " map:λ(id x, ", this.toDynamicExpression(arrayType.items, "x"), ")]"];
            },
            _classType => ["[", dynamic, " JSONDictionary]"],
            mapType => {
                if (this.isJSONSafe(mapType)) {
                    // TODO check each value type
                    return dynamic;
                }
                return ["[", dynamic, " map:λ(id x, ", this.toDynamicExpression(mapType.values, "x"), ")]"];
            },
            _enumType => ["[", dynamic, " value]"],
            unionType => {
                const nullable = nullableFromUnion(unionType);
                if (nullable !== null) {
                    if (this.isJSONSafe(nullable)) {
                        return ["NSNullify(", dynamic, ")"];
                    } else {
                        return ["NSNullify(", this.toDynamicExpression(nullable, dynamic), ")"];
                    }
                } else {
                    // TODO support unions
                    return dynamic;
                }
            }
        );
    };<commit_msg>Better name<commit_after>"use strict";

import { includes, startsWith } from "lodash";
import * as pluralize from "pluralize";

import { TargetLanguage } from "../TargetLanguage";
import { Type, ClassType, EnumType, nullableFromUnion, matchType, ArrayType, MapType, UnionType } from "../Type";
import { TypeGraph } from "../TypeGraph";
import { Namespace, Name, Namer, funPrefixNamer } from "../Naming";
import { Sourcelike, modifySource } from "../Source";
import {
    splitIntoWords,
    combineWords,
    firstUpperWordStyle,
    allUpperWordStyle,
    allLowerWordStyle,
    camelCase,
    utf16LegalizeCharacters,
    stringEscape
} from "../Strings";
import { ConvenienceRenderer } from "../ConvenienceRenderer";
import { StringOption, BooleanOption, EnumOption } from "../RendererOptions";
import { Set } from "immutable";

const unicode = require("unicode-properties");

type OutputFeatures = { interface: boolean; implementation: boolean };

export default class ObjectiveCTargetLanguage extends TargetLanguage {
    private readonly _featuresOption = new EnumOption("features", "Interface and implementation", [
        ["all", { interface: true, implementation: true }],
        ["interface", { interface: true, implementation: false }],
        ["implementation", { interface: false, implementation: true }]
    ]);
    private readonly _justTypesOption = new BooleanOption("just-types", "Plain types only", false);
    private readonly _classPrefixOption = new StringOption("class-prefix", "Class prefix", "PREFIX", "QT");
    private readonly _extraCommentsOption = new BooleanOption("extra-comments", "Extra comments", false);

    constructor() {
        super("Objective-C", ["objc", "objective-c", "objectivec"], "m");
        this.setOptions([
            this._justTypesOption,
            this._classPrefixOption,
            this._featuresOption,
            this._extraCommentsOption
        ]);
    }

    protected get rendererClass(): new (
        graph: TypeGraph,
        leadingComments: string[] | undefined,
        ...optionValues: any[]
    ) => ConvenienceRenderer {
        return ObjectiveCRenderer;
    }
}

function typeNameStyle(prefix: string, original: string): string {
    const words = splitIntoWords(original);
    const result = combineWords(
        words,
        legalizeName,
        firstUpperWordStyle,
        firstUpperWordStyle,
        allUpperWordStyle,
        allUpperWordStyle,
        "",
        isStartCharacter
    );
    // Take care not to doubly-prefix type names
    return startsWith(result, prefix) ? result : prefix + result;
}

function propertyNameStyle(original: string): string {
    const words = splitIntoWords(original);
    return combineWords(
        words,
        legalizeName,
        allLowerWordStyle,
        firstUpperWordStyle,
        allLowerWordStyle,
        allUpperWordStyle,
        "",
        isStartCharacter
    );
}

const keywords = [
    "_Bool",
    "_Complex",
    "_Imaginary",
    "atomic",
    "auto",
    "break",
    "case ",
    "char",
    "const",
    "continue",
    "default",
    "do",
    "double",
    "else",
    "enum",
    "extern",
    "false",
    "float ",
    "for ",
    "goto",
    "if",
    "inline",
    "int",
    "long",
    "nil",
    "nonatomic",
    "register",
    "restrict",
    "retain",
    "return",
    "short",
    "signed",
    "sizeof",
    "static",
    "struct",
    "switch",
    "typedef",
    "true",
    "union",
    "unsigned",
    "void",
    "volatile",
    "while"
];

const propertySafeKeywords = [
    "BOOL",
    "Class",
    "bycopy",
    "byref",
    "decimal",
    "id",
    "IMP",
    "in",
    "inout",
    "NO",
    "NULL",
    "oneway",
    "out",
    "Protocol",
    "SEL",
    "self",
    "super",
    "YES"
];

function isStartCharacter(utf16Unit: number): boolean {
    return unicode.isAlphabetic(utf16Unit) || utf16Unit === 0x5f; // underscore
}

function isPartCharacter(utf16Unit: number): boolean {
    const category: string = unicode.getCategory(utf16Unit);
    return includes(["Nd", "Pc", "Mn", "Mc"], category) || isStartCharacter(utf16Unit);
}

const legalizeName = utf16LegalizeCharacters(isPartCharacter);

function isAnyOrNull(t: Type): boolean {
    return t.kind === "any" || t.kind === "null";
}

class ObjectiveCRenderer extends ConvenienceRenderer {
    // enums contained in NSArray or NSDictionary are represented
    // as 'pseudo-enum' reference types. Eventually we may want to
    // support 'natural' enums (NSUInteger) but this isn't implemented yet.
    pseudoEnums: Set<EnumType>;

    constructor(
        graph: TypeGraph,
        leadingComments: string[] | undefined,
        private readonly _justTypes: boolean,
        private readonly _classPrefix: string,
        private readonly _features: OutputFeatures,
        private readonly _extraComments: boolean
    ) {
        super(graph, leadingComments);

        this.pseudoEnums = graph
            .allTypesUnordered()
            .filter(t => t instanceof EnumType && this.enumOccursInArrayOrMap(t))
            .map(t => t as EnumType);
    }

    private enumOccursInArrayOrMap(enumType: EnumType): boolean {
        function containedBy(t: Type): boolean {
            return (
                (t instanceof ArrayType && t.items.equals(enumType)) ||
                (t instanceof MapType && t.values.equals(enumType)) ||
                (t instanceof ClassType && t.properties.some(containedBy)) ||
                // TODO support unions
                (t instanceof UnionType && false)
            );
        }
        return this.typeGraph.allTypesUnordered().some(containedBy);
    }

    protected get forbiddenNamesForGlobalNamespace(): string[] {
        return keywords;
    }

    protected forbiddenForClassProperties(
        _c: ClassType,
        _classNamed: Name
    ): { names: Name[]; namespaces: Namespace[] } {
        return { names: [], namespaces: [this.forbiddenWordsNamespace] };
    }

    protected forbiddenForEnumCases(_e: EnumType, _enumNamed: Name): { names: Name[]; namespaces: Namespace[] } {
        return { names: [], namespaces: [this.forbiddenWordsNamespace] };
    }

    protected topLevelNameStyle(rawName: string): string {
        return camelCase(rawName);
    }

    protected makeNamedTypeNamer(): Namer {
        return funPrefixNamer(rawName => typeNameStyle(this._classPrefix, rawName));
    }

    protected makeClassPropertyNamer(): Namer {
        // TODO why is underscore being removed?
        return new Namer(s => propertyNameStyle(s), ["_", "the", "one", "some", "another"]);
    }

    protected makeUnionMemberNamer(): null {
        return null;
    }

    protected makeEnumCaseNamer(): Namer {
        return new Namer(propertyNameStyle, []);
    }

    protected namedTypeToNameForTopLevel(type: Type): Type | undefined {
        return type;
    }

    private emitBlock = (line: Sourcelike, f: () => void): void => {
        this.emitLine(line, " {");
        this.indent(f);
        this.emitLine("}");
    };

    private emitMethod(declaration: Sourcelike, f: () => void) {
        this.emitLine(declaration);
        this.emitLine("{");
        this.indent(f);
        this.emitLine("}");
    }

    private emitExtraComments = (...comments: Sourcelike[]) => {
        if (!this._extraComments) return;
        for (const comment of comments) {
            this.emitLine("// ", comment);
        }
    };

    private objcType = (t: Type): [Sourcelike, string] => {
        return matchType<[Sourcelike, string]>(
            t,
            _anyType => ["id", ""],
            // For now, we're treating nulls just like any
            _nullType => ["id", ""],
            _boolType => ["NSBoolean", " *"],
            _integerType => ["NSNumber", " *"],
            _doubleType => ["NSNumber", " *"],
            _stringType => ["NSString", " *"],
            arrayType => {
                const itemType = arrayType.items;
                const itemTypeName = this.objcType(itemType);
                // NSArray<id>* === NSArray*
                if (isAnyOrNull(itemType)) {
                    return ["NSArray", " *"];
                }
                return [["NSArray<", itemTypeName, ">"], " *"];
            },
            classType => [this.nameForNamedType(classType), " *"],
            mapType => [["NSDictionary<NSString *, ", this.objcType(mapType.values), ">"], " *"],
            enumType => [this.nameForNamedType(enumType), " *"],
            unionType => {
                const nullable = nullableFromUnion(unionType);
                return nullable !== null ? this.objcType(nullable) : ["id", ""];
            }
        );
    };

    private jsonType = (t: Type): [Sourcelike, string] => {
        return matchType<[Sourcelike, string]>(
            t,
            _anyType => ["id", ""],
            // For now, we're treating nulls just like any
            _nullType => ["id", ""],
            _boolType => ["NSBoolean", " *"],
            _integerType => ["NSNumber", " *"],
            _doubleType => ["NSNumber", " *"],
            _stringType => ["NSString", " *"],
            _arrayType => ["NSArray", " *"],
            _classType => ["NSDictionary<NSString *, id>", " *"],
            mapType => [["NSDictionary<NSString *, ", this.jsonType(mapType.values), ">"], " *"],
            _enumType => ["NSString", " *"],
            unionType => {
                const nullable = nullableFromUnion(unionType);
                return nullable !== null ? this.jsonType(nullable) : ["id", ""];
            }
        );
    };

    private fromDynamicExpression = (t: Type, ...dynamic: Sourcelike[]): Sourcelike => {
        return matchType<Sourcelike>(
            t,
            _anyType => dynamic,
            _nullType => dynamic,
            _boolType => dynamic,
            _integerType => dynamic,
            _doubleType => dynamic,
            _stringType => dynamic,
            arrayType => ["[", dynamic, " map:λ(id x, ", this.fromDynamicExpression(arrayType.items, "x"), ")]"],
            classType => ["[", this.nameForNamedType(classType), " fromJSONDictionary:", dynamic, "]"],
            mapType => ["[", dynamic, " map:λ(id x, ", this.fromDynamicExpression(mapType.values, "x"), ")]"],
            enumType => ["NOT_NIL([", this.nameForNamedType(enumType), " withValue:", dynamic, "])"],
            unionType => {
                const nullable = nullableFromUnion(unionType);
                return nullable !== null ? this.fromDynamicExpression(nullable, dynamic) : dynamic;
            }
        );
    };

    private toDynamicExpression = (t: Type, typed: Sourcelike): Sourcelike => {
        return matchType<Sourcelike>(
            t,
            _anyType => ["NSNullify(", typed, ")"],
            _nullType => ["NSNullify(", typed, ")"],
            _boolType => typed,
            _integerType => typed,
            _doubleType => typed,
            _stringType => typed,
            arrayType => {
                if (this.isJSONSafe(arrayType)) {
                    // TODO check each value type
                    return typed;
                }
                return ["[", typed, " map:λ(id x, ", this.toDynamicExpression(arrayType.items, "x"), ")]"];
            },
            _classType => ["[", typed, " JSONDictionary]"],
            mapType => {
                if (this.isJSONSafe(mapType)) {
                    // TODO check each value type
                    return typed;
                }
                return ["[", typed, " map:λ(id x, ", this.toDynamicExpression(mapType.values, "x"), ")]"];
            },
            _enumType => ["[", typed, " value]"],
            unionType => {
                const nullable = nullableFromUnion(unionType);
                if (nullable !== null) {
                    if (this.isJSONSafe(nullable)) {
                        return ["NSNullify(", typed, ")"];
                    } else {
                        return ["NSNullify(", this.toDynamicExpression(nullable, typed), ")"];
                    }
                } else {
                    // TODO support unions
                    return typed;
                }
            }
        );
    };
{'fraction_non_alphanumeric': 0.14083701160699688, 'fraction_numerical': 0.0014713094654242277, 'mean_word_length': 2.1827523413111343, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 137, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '331317', 'n_tokens_mistral': 6913, 'n_tokens_neox': 6695, 'n_words': 1535}