blob: eaa56c539c5185ac94cc71a12869be04e1ef5dbe [file] [log] [blame] [edit]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:logging/logging.dart';
import 'package:pub_semver/pub_semver.dart';
import '../code_generator.dart';
import '../config_provider/config_types.dart';
import '../context.dart';
import 'clang_bindings/clang_bindings.dart' as clang_types;
import 'type_extractor/extractor.dart';
const exceptionalVisitorReturn =
clang_types.CXChildVisitResult.CXChildVisit_Break;
typedef _CursorVisitorCallback =
Int32 Function(clang_types.CXCursor, clang_types.CXCursor, Pointer<Void>);
/// Logs the warnings/errors returned by clang for a translation unit.
void logTuDiagnostics(
Pointer<clang_types.CXTranslationUnitImpl> tu,
Context context,
String header, {
Level logLevel = Level.SEVERE,
}) {
final total = clang.clang_getNumDiagnostics(tu);
if (total == 0) {
return;
}
context.logger.log(
logLevel,
'Header $header: Total errors/warnings: $total.',
);
for (var i = 0; i < total; i++) {
final diag = clang.clang_getDiagnostic(tu, i);
if (clang.clang_getDiagnosticSeverity(diag) >=
clang_types.CXDiagnosticSeverity.CXDiagnostic_Warning) {
context.hasSourceErrors = true;
}
final cxstring = clang.clang_formatDiagnostic(
diag,
clang_types
.CXDiagnosticDisplayOptions
.CXDiagnostic_DisplaySourceLocation |
clang_types.CXDiagnosticDisplayOptions.CXDiagnostic_DisplayColumn |
clang_types
.CXDiagnosticDisplayOptions
.CXDiagnostic_DisplayCategoryName,
);
context.logger.log(logLevel, ' ${cxstring.toStringAndDispose()}');
clang.clang_disposeDiagnostic(diag);
}
}
extension CXSourceLocationExt on clang_types.CXSourceLocation {
(String, int) get fileAndOffset {
final filePtr = calloc<Pointer<Void>>();
final offsetPtr = calloc<UnsignedInt>();
clang.clang_getFileLocation(this, filePtr, nullptr, nullptr, offsetPtr);
final file = clang.clang_getFileName(filePtr.value).toStringAndDispose();
final offset = offsetPtr.value;
calloc.free(filePtr);
calloc.free(offsetPtr);
return (file, offset);
}
}
extension CXSourceRangeExt on clang_types.CXSourceRange {
clang_types.CXSourceLocation get start => clang.clang_getRangeStart(this);
clang_types.CXSourceLocation get end => clang.clang_getRangeEnd(this);
((String, int), (String, int)) toTuple() =>
(start.fileAndOffset, end.fileAndOffset);
}
extension CXSourceRangePtrExt on Pointer<clang_types.CXSourceRange> {
void dispose() {
calloc.free(this);
}
}
extension CXCursorExt on clang_types.CXCursor {
String usr() {
var res = clang.clang_getCursorUSR(this).toStringAndDispose();
if (isAnonymousRecordDecl()) {
res += '@offset:${sourceFileOffset()}';
}
return res;
}
/// Returns the kind int from [clang_types.CXCursorKind].
int kind() {
return clang.clang_getCursorKind(this);
}
/// Name of the cursor (E.g function name, Struct name, Parameter name).
String spelling() {
return clang.clang_getCursorSpelling(this).toStringAndDispose();
}
/// Spelling for a [clang_types.CXCursorKind], useful for debug purposes.
String kindSpelling() {
return clang
.clang_getCursorKindSpelling(clang.clang_getCursorKind(this))
.toStringAndDispose();
}
/// Get code_gen [Type] representation of [clang_types.CXType].
Type toCodeGenType(Context context) {
return getCodeGenType(context, type(), originalCursor: this);
}
/// for debug: returns [spelling] [kind] [kindSpelling] type typeSpelling.
String completeStringRepr() {
final cxtype = type();
final s =
'(Cursor) spelling: ${spelling()}, kind: ${kind()}, '
'kindSpelling: ${kindSpelling()}, type: ${cxtype.kind}, '
'typeSpelling: ${cxtype.spelling()}, usr: ${usr()}';
return s;
}
/// Type associated with the pointer if any. Type will have kind
/// clang.CXTypeKind.CXType_Invalid otherwise.
clang_types.CXType type() {
return clang.clang_getCursorType(this);
}
/// Determine whether the given cursor
/// represents an anonymous record declaration.
bool isAnonymousRecordDecl() {
return clang.clang_Cursor_isAnonymousRecordDecl(this) == 1;
}
/// Only valid for clang.CXCursorKind.CXCursor_FunctionDecl. Type will have
/// kind clang.CXTypeKind.CXType_Invalid otherwise.
clang_types.CXType returnType() {
return clang.clang_getResultType(type());
}
/// Returns the file name of the file that the cursor is inside.
String sourceFileName() {
final cxsource = clang.clang_getCursorLocation(this);
final cxfilePtr = calloc<Pointer<Void>>();
// Puts the values in these pointers.
clang.clang_getFileLocation(cxsource, cxfilePtr, nullptr, nullptr, nullptr);
final s = clang.clang_getFileName(cxfilePtr.value).toStringAndDispose();
calloc.free(cxfilePtr);
return s;
}
int sourceFileOffset() {
final cxsource = clang.clang_getCursorLocation(this);
final cxOffset = calloc<UnsignedInt>();
// Puts the values in these pointers.
clang.clang_getFileLocation(cxsource, nullptr, nullptr, nullptr, cxOffset);
final offset = cxOffset.value;
calloc.free(cxOffset);
return offset;
}
/// Returns whether the file that the cursor is inside is a system header.
bool isInSystemHeader() {
final location = clang.clang_getCursorLocation(this);
return clang.clang_Location_isInSystemHeader(location) != 0;
}
/// Visits all the direct children of this cursor.
///
/// [callback] is called with the child cursor. The iteration continues until
/// completion. The only way it can be interrupted is if the [callback]
/// throws, in which case this method also throws.
void visitChildren(void Function(clang_types.CXCursor child) callback) {
final completed = visitChildrenMayBreak((clang_types.CXCursor child) {
callback(child);
return true;
});
if (!completed) {
throw Exception(
'Exception thrown in a dart function called via C, '
'use --verbose to see more details',
);
}
}
/// Visits all the direct children of this cursor.
///
/// [callback] is called with the child cursor. If [callback] returns true,
/// the iteration will continue. Otherwise, if [callback] returns false, the
/// iteration will stop.
///
/// Returns whether the iteration completed.
bool visitChildrenMayBreak(
bool Function(clang_types.CXCursor child) callback,
) => visitChildrenMayRecurse(
(clang_types.CXCursor child, clang_types.CXCursor parent) => callback(child)
? clang_types.CXChildVisitResult.CXChildVisit_Continue
: clang_types.CXChildVisitResult.CXChildVisit_Break,
);
/// Visits all the direct children of this cursor.
///
/// [callback] is called with the child cursor and parent cursor. If
/// [callback] returns CXChildVisit_Continue, the iteration continues. If it
/// returns CXChildVisit_Break the iteration stops. If it returns
/// CXChildVisit_Recurse, the iteration recurses into the node.
///
/// Returns whether the iteration completed.
bool visitChildrenMayRecurse(
int Function(clang_types.CXCursor child, clang_types.CXCursor parent)
callback,
) {
final visitor = NativeCallable<_CursorVisitorCallback>.isolateLocal(
(
clang_types.CXCursor child,
clang_types.CXCursor parent,
Pointer<Void> clientData,
) => callback(child, parent),
exceptionalReturn: exceptionalVisitorReturn,
);
final result = clang.clang_visitChildren(
this,
visitor.nativeFunction.cast(),
nullptr,
);
visitor.close();
return result == 0;
}
/// Returns the first child where `predicate(child)` is true, or null if there
/// isn't one.
clang_types.CXCursor? findChildWhere(
bool Function(clang_types.CXCursor) predicate,
) {
clang_types.CXCursor? result;
visitChildrenMayBreak((child) {
if (predicate(child)) {
result = child;
return false;
}
return true;
});
return result;
}
/// Returns the first child with the given CXCursorKind, or null if there
/// isn't one.
clang_types.CXCursor? findChildWithKind(int kind) =>
findChildWhere((child) => child.kind == kind);
/// Returns whether there is a child with the given CXCursorKind.
bool hasChildWithKind(int kind) => findChildWithKind(kind) != null;
/// Recursively print the AST, for debugging.
void printAst([int maxDepth = 3]) => _printAst(maxDepth, 0);
void _printAst(int maxDepth, int depth) {
if (depth > maxDepth) {
return;
}
print((' ' * depth) + completeStringRepr());
visitChildren((child) => child._printAst(maxDepth, depth + 1));
}
}
const commentPrefix = '/// ';
const nesting = ' ';
/// Returns a cursor's associated comment.
///
/// The given string is wrapped at line width = 80 - [indent]. The [indent] is
/// [commentPrefix].length by default because a comment starts with
/// [commentPrefix].
String? getCursorDocComment(
Context context,
clang_types.CXCursor cursor, {
int indent = commentPrefix.length,
String? fallbackComment,
String? availability,
}) {
String? formattedDocComment;
final currentCommentRange = clang.clang_Cursor_getCommentRange(cursor);
// Only report the comment if we haven't reported this comment before.
if (!context.reportedCommentRanges.add(currentCommentRange.toTuple())) {
formattedDocComment = null;
} else {
switch (context.config.commentType.length) {
case CommentLength.full:
formattedDocComment = removeRawCommentMarkups(
clang.clang_Cursor_getRawCommentText(cursor).toStringAndDispose(),
);
break;
case CommentLength.brief:
formattedDocComment = _wrapNoNewLineString(
clang.clang_Cursor_getBriefCommentText(cursor).toStringAndDispose(),
80 - indent,
);
break;
default:
formattedDocComment = null;
}
}
final docs = [formattedDocComment ?? fallbackComment, availability].nonNulls;
return docs.isEmpty ? null : docs.join('\n\n');
}
/// Wraps [string] according to given [lineWidth].
///
/// Wrapping will work properly only when String has no new lines
/// characters(\n).
String? _wrapNoNewLineString(String? string, int lineWidth) {
if (string == null || string.isEmpty) {
return null;
}
final sb = StringBuffer();
final words = string.split(' ');
sb.write(words[0]);
var trackLineWidth = words[0].length;
for (var i = 1; i < words.length; i++) {
final word = words[i];
if (trackLineWidth + word.length < lineWidth) {
sb.write(' ');
sb.write(word);
trackLineWidth += word.length + 1;
} else {
sb.write('\n');
sb.write(word);
trackLineWidth = word.length;
}
}
return sb.toString();
}
/// Removes /*, */ and any *'s in the beginning of a line.
String? removeRawCommentMarkups(String? string) {
if (string == null || string.isEmpty) {
return null;
}
final sb = StringBuffer();
// Remove comment identifiers (`/** * */`, `///`, `//`) from lines.
if (string.contains(RegExp(r'^\s*\/\*+'))) {
string = string.replaceFirst(RegExp(r'^\s*\/\*+\s*'), '');
string = string.replaceFirst(RegExp(r'\s*\*+\/$'), '');
string.split('\n').forEach((element) {
element = element.replaceFirst(RegExp(r'^\s*\**\s*'), '');
sb.writeln(element);
});
} else if (string.contains(RegExp(r'^\s*\/\/\/?\s*'))) {
string.split('\n').forEach((element) {
element = element.replaceFirst(RegExp(r'^\s*\/\/\/?\s*'), '');
sb.writeln(element);
});
}
return sb.toString().trim();
}
extension CXTypeExt on clang_types.CXType {
/// Get code_gen [Type] representation of [clang_types.CXType].
Type toCodeGenType(Context context, {bool supportNonInlineArray = false}) {
return getCodeGenType(
context,
this,
supportNonInlineArray: supportNonInlineArray,
);
}
/// Spelling for a [clang_types.CXTypeKind], useful for debug purposes.
String spelling() {
return clang.clang_getTypeSpelling(this).toStringAndDispose();
}
/// Returns the typeKind int from [clang_types.CXTypeKind].
int kind() {
return this.kind;
}
String kindSpelling() {
return clang.clang_getTypeKindSpelling(kind()).toStringAndDispose();
}
int alignment() {
return clang.clang_Type_getAlignOf(this);
}
/// For debugging: returns [spelling] [kind] [kindSpelling].
String completeStringRepr() {
final s =
'(Type) spelling: ${spelling()}, kind: ${kind()}, '
'kindSpelling: ${kindSpelling()}';
return s;
}
bool get isConstQualified {
return clang.clang_isConstQualifiedType(this) != 0;
}
}
extension CXStringExt on clang_types.CXString {
/// Convert CXString to a Dart string
///
/// Make sure to dispose CXstring using dispose method, or use the
/// [toStringAndDispose] method.
String string() {
final cstring = clang.clang_getCString(this);
if (cstring != nullptr) {
return cstring.cast<Utf8>().toDartString();
} else {
return '';
}
}
/// Converts CXString to dart string and disposes CXString.
String toStringAndDispose() {
// Note: clang_getCString_wrap returns a const char *, calling free will
// result in error.
final s = string();
clang.clang_disposeString(this);
return s;
}
void dispose() {
clang.clang_disposeString(this);
}
}
extension CXVersionExt on clang_types.CXVersion {
Version? get triple {
// -1 can appear in a CXVersion, and has various meanings.
// Whenever one of the fields is -1, the subsequent fields are also -1 (eg
// you can't have Minor=-1 and Subminor=4). If all 3 fields are -1, it means
// "no version", and we return null. Otherwise, we treat a -1 in any field
// as a 0.
if (Major < 0) return null;
return Version(Major, Minor < 0 ? 0 : Minor, Subminor < 0 ? 0 : Subminor);
}
String string() => '$Major.$Minor.$Subminor';
}
/// Converts a [List<String>] to [Pointer<Pointer<Utf8>>].
Pointer<Pointer<Utf8>> createDynamicStringArray(List<String> list) {
final nativeCmdArgs = calloc<Pointer<Utf8>>(list.length);
for (var i = 0; i < list.length; i++) {
nativeCmdArgs[i] = list[i].toNativeUtf8();
}
return nativeCmdArgs;
}
extension DynamicCStringArray on Pointer<Pointer<Utf8>> {
// Properly disposes a Pointer<Pointer<Utf8>, ensure that sure length is
// correct.
void dispose(int length) {
for (var i = 0; i < length; i++) {
calloc.free(this[i]);
}
calloc.free(this);
}
}
class Stack<T> {
final _stack = <T>[];
T get top => _stack.last;
T pop() => _stack.removeLast();
void push(T item) => _stack.add(item);
}
class Macro {
final String usr;
final String? originalName;
Macro(this.usr, this.originalName);
}
/// Tracks if a binding is 'seen' or not.
class BindingsIndex {
// Tracks if bindings are already seen, Map key is USR obtained from libclang.
final Map<String, Type> _declaredTypes = {};
final Map<String, Func> _functions = {};
final Map<String, Constant> _unnamedEnumConstants = {};
final Map<String, String> _macros = {};
final Map<String, Global> _globals = {};
final Map<String, ObjCBlock> _objcBlocks = {};
final Map<String, ObjCProtocol> _objcProtocols = {};
final Map<String, ObjCCategory> _objcCategories = {};
/// Contains usr for typedefs which cannot be generated.
final Set<String> _unsupportedTypealiases = {};
/// Index for headers.
final Map<String, bool> _headerCache = {};
bool isSeenType(String usr) => _declaredTypes.containsKey(usr);
void addTypeToSeen(String usr, Type type) => _declaredTypes[usr] = type;
Type? getSeenType(String usr) => _declaredTypes[usr];
bool isSeenFunc(String usr) => _functions.containsKey(usr);
void addFuncToSeen(String usr, Func func) => _functions[usr] = func;
Func? getSeenFunc(String usr) => _functions[usr];
bool isSeenUnnamedEnumConstant(String usr) =>
_unnamedEnumConstants.containsKey(usr);
void addUnnamedEnumConstantToSeen(String usr, Constant enumConstant) =>
_unnamedEnumConstants[usr] = enumConstant;
Constant? getSeenUnnamedEnumConstant(String usr) =>
_unnamedEnumConstants[usr];
bool isSeenGlobalVar(String usr) => _globals.containsKey(usr);
void addGlobalVarToSeen(String usr, Global global) => _globals[usr] = global;
Global? getSeenGlobalVar(String usr) => _globals[usr];
bool isSeenMacro(String usr) => _macros.containsKey(usr);
void addMacroToSeen(String usr, String macro) => _macros[usr] = macro;
String? getSeenMacro(String usr) => _macros[usr];
bool isSeenUnsupportedTypealias(String usr) =>
_unsupportedTypealiases.contains(usr);
void addUnsupportedTypealiasToSeen(String usr) =>
_unsupportedTypealiases.add(usr);
bool isSeenHeader(String source) => _headerCache.containsKey(source);
void addHeaderToSeen(String source, bool includeStatus) =>
_headerCache[source] = includeStatus;
bool? getSeenHeaderStatus(String source) => _headerCache[source];
void addObjCBlockToSeen(String key, ObjCBlock t) => _objcBlocks[key] = t;
ObjCBlock? getSeenObjCBlock(String key) => _objcBlocks[key];
void addObjCProtocolToSeen(String usr, ObjCProtocol t) =>
_objcProtocols[usr] = t;
ObjCProtocol? getSeenObjCProtocol(String usr) => _objcProtocols[usr];
bool isSeenObjCProtocol(String usr) => _objcProtocols.containsKey(usr);
void addObjCCategoryToSeen(String usr, ObjCCategory t) =>
_objcCategories[usr] = t;
ObjCCategory? getSeenObjCCategory(String usr) => _objcCategories[usr];
bool isSeenObjCCategory(String usr) => _objcCategories.containsKey(usr);
}
class CursorIndex {
final Logger _logger;
final _usrCursorDefinition = <String, clang_types.CXCursor>{};
CursorIndex(this._logger);
/// Returns the Cursor definition (if found) or itself.
clang_types.CXCursor getDefinition(clang_types.CXCursor cursor) {
final cursorDefinition = clang.clang_getCursorDefinition(cursor);
if (clang.clang_Cursor_isNull(cursorDefinition) == 0) {
return cursorDefinition;
} else {
final usr = cursor.usr();
if (_usrCursorDefinition.containsKey(usr)) {
return _usrCursorDefinition[cursor.usr()]!;
} else {
_logger.warning(
'No definition found for declaration -'
'${cursor.completeStringRepr()}',
);
return cursor;
}
}
}
/// Saves cursor definition based on its kind.
void saveDefinition(clang_types.CXCursor cursor) {
switch (cursor.kind) {
case clang_types.CXCursorKind.CXCursor_StructDecl:
case clang_types.CXCursorKind.CXCursor_UnionDecl:
case clang_types.CXCursorKind.CXCursor_EnumDecl:
final usr = cursor.usr();
if (!_usrCursorDefinition.containsKey(usr)) {
final cursorDefinition = clang.clang_getCursorDefinition(cursor);
if (clang.clang_Cursor_isNull(cursorDefinition) == 0) {
_usrCursorDefinition[usr] = cursorDefinition;
} else {
_logger.finest(
'Missing cursor definition in current translation unit: '
'${cursor.completeStringRepr()}',
);
}
}
}
}
}