TypeConverter.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Reflection;
  5. using System.Runtime.CompilerServices;
  6. using Esprima.Ast;
  7. using Jint.Native;
  8. using Jint.Native.Number;
  9. using Jint.Native.Number.Dtoa;
  10. using Jint.Native.Object;
  11. using Jint.Native.String;
  12. using Jint.Native.Symbol;
  13. using Jint.Pooling;
  14. namespace Jint.Runtime
  15. {
  16. [Flags]
  17. public enum Types
  18. {
  19. None = 0,
  20. Undefined = 1,
  21. Null = 2,
  22. Boolean = 4,
  23. String = 8,
  24. Number = 16,
  25. Symbol = 64,
  26. Object = 128,
  27. Completion = 256
  28. }
  29. [Flags]
  30. internal enum InternalTypes
  31. {
  32. None = 0,
  33. Undefined = 1,
  34. Null = 2,
  35. Boolean = 4,
  36. String = 8,
  37. Number = 16,
  38. Integer = 32,
  39. Symbol = 64,
  40. Object = 128,
  41. Completion = 256
  42. }
  43. public static class TypeConverter
  44. {
  45. // how many decimals to check when determining if double is actually an int
  46. private const double DoubleIsIntegerTolerance = double.Epsilon * 100;
  47. internal static readonly string[] intToString = new string[1024];
  48. private static readonly string[] charToString = new string[256];
  49. static TypeConverter()
  50. {
  51. for (var i = 0; i < intToString.Length; ++i)
  52. {
  53. intToString[i] = i.ToString();
  54. }
  55. for (var i = 0; i < charToString.Length; ++i)
  56. {
  57. var c = (char) i;
  58. charToString[i] = c.ToString();
  59. }
  60. }
  61. /// <summary>
  62. /// http://www.ecma-international.org/ecma-262/#sec-toprimitive
  63. /// </summary>
  64. public static JsValue ToPrimitive(JsValue input, Types preferredType = Types.None)
  65. {
  66. if (!(input is ObjectInstance oi))
  67. {
  68. return input;
  69. }
  70. var hint = preferredType switch
  71. {
  72. Types.String => JsString.StringString,
  73. Types.Number => JsString.NumberString,
  74. _ => JsString.DefaultString
  75. };
  76. var exoticToPrim = GetMethod(oi, GlobalSymbolRegistry.ToPrimitive);
  77. if (exoticToPrim is object)
  78. {
  79. var str = exoticToPrim.Call(oi, new JsValue[] { hint });
  80. if (str.IsPrimitive())
  81. {
  82. return str;
  83. }
  84. if (str.IsObject())
  85. {
  86. return ExceptionHelper.ThrowTypeError<JsValue>(oi.Engine, "Cannot convert object to primitive value");
  87. }
  88. }
  89. return OrdinaryToPrimitive(oi, preferredType == Types.None ? Types.Number : preferredType);
  90. }
  91. private static readonly Key[] StringHintCallOrder = { (Key) "toString", (Key) "valueOf"};
  92. private static readonly Key[] NumberHintCallOrder = { (Key) "valueOf", (Key) "toString"};
  93. /// <summary>
  94. /// http://www.ecma-international.org/ecma-262/#sec-ordinarytoprimitive
  95. /// </summary>
  96. internal static JsValue OrdinaryToPrimitive(ObjectInstance input, Types hint = Types.None)
  97. {
  98. var callOrder = ArrayExt.Empty<Key>();
  99. if (hint == Types.String)
  100. {
  101. callOrder = StringHintCallOrder;
  102. }
  103. if (hint == Types.Number)
  104. {
  105. callOrder = NumberHintCallOrder;
  106. }
  107. foreach (var property in callOrder)
  108. {
  109. var method = input.Get(property) as ICallable;
  110. if (method is object)
  111. {
  112. var val = method.Call(input, Arguments.Empty);
  113. if (val.IsPrimitive())
  114. {
  115. return val;
  116. }
  117. }
  118. }
  119. return ExceptionHelper.ThrowTypeError<JsValue>(input.Engine);
  120. }
  121. internal static ICallable GetMethod(ObjectInstance v, in Key p)
  122. {
  123. var jsValue = v.Get(p);
  124. if (jsValue.IsNullOrUndefined())
  125. {
  126. return null;
  127. }
  128. if (!(jsValue is ICallable callable))
  129. {
  130. return ExceptionHelper.ThrowTypeError<ICallable>(v.Engine, "Value returned for property '" + p.Name + "' of object is not a function");
  131. }
  132. return callable;
  133. }
  134. /// <summary>
  135. /// http://www.ecma-international.org/ecma-262/5.1/#sec-9.2
  136. /// </summary>
  137. public static bool ToBoolean(JsValue o)
  138. {
  139. switch (o._type)
  140. {
  141. case InternalTypes.Boolean:
  142. return ((JsBoolean) o)._value;
  143. case InternalTypes.Undefined:
  144. case InternalTypes.Null:
  145. return false;
  146. case InternalTypes.Integer:
  147. return (int) ((JsNumber) o)._value != 0;
  148. case InternalTypes.Number:
  149. var n = ((JsNumber) o)._value;
  150. return n != 0 && !double.IsNaN(n);
  151. case InternalTypes.String:
  152. return !((JsString) o).IsNullOrEmpty();
  153. default:
  154. return true;
  155. }
  156. }
  157. /// <summary>
  158. /// http://www.ecma-international.org/ecma-262/5.1/#sec-9.3
  159. /// </summary>
  160. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  161. public static double ToNumber(JsValue o)
  162. {
  163. return o.IsNumber()
  164. ? ((JsNumber) o)._value
  165. : ToNumberUnlikely(o);
  166. }
  167. private static double ToNumberUnlikely(JsValue o)
  168. {
  169. switch (o._type)
  170. {
  171. case InternalTypes.Undefined:
  172. return double.NaN;
  173. case InternalTypes.Null:
  174. return 0;
  175. case InternalTypes.Object when o is IPrimitiveInstance p:
  176. return ToNumber(ToPrimitive(p.PrimitiveValue, Types.Number));
  177. case InternalTypes.Boolean:
  178. return ((JsBoolean) o)._value ? 1 : 0;
  179. case InternalTypes.String:
  180. return ToNumber(o.AsStringWithoutTypeCheck());
  181. case InternalTypes.Symbol:
  182. // TODO proper TypeError would require Engine instance and a lot of API changes
  183. return ExceptionHelper.ThrowTypeErrorNoEngine<double>("Cannot convert a Symbol value to a number");
  184. default:
  185. return ToNumber(ToPrimitive(o, Types.Number));
  186. }
  187. }
  188. internal static bool CanBeIndex(in Key input)
  189. {
  190. if (input.Name.Length == 0)
  191. {
  192. return false;
  193. }
  194. var c = input.Name[0];
  195. return char.IsDigit(c) || c == ' ' || c == '+' || c == '-' || c == 'I';
  196. }
  197. internal static double ToNumber(string input)
  198. {
  199. // eager checks to save time and trimming
  200. if (string.IsNullOrEmpty(input))
  201. {
  202. return 0;
  203. }
  204. char first = input[0];
  205. if (input.Length == 1 && first >= '0' && first <= '9')
  206. {
  207. // simple constant number
  208. return first - '0';
  209. }
  210. var s = StringPrototype.IsWhiteSpaceEx(input[0]) || StringPrototype.IsWhiteSpaceEx(input[input.Length - 1])
  211. ? StringPrototype.TrimEx(input)
  212. : input;
  213. if (s.Length == 0)
  214. {
  215. return 0;
  216. }
  217. if (s.Length == 8 || s.Length == 9)
  218. {
  219. if ("+Infinity" == s || "Infinity" == s)
  220. {
  221. return double.PositiveInfinity;
  222. }
  223. if ("-Infinity" == s)
  224. {
  225. return double.NegativeInfinity;
  226. }
  227. }
  228. // todo: use a common implementation with JavascriptParser
  229. try
  230. {
  231. if (s.Length > 2 && s[0] == '0' && char.IsLetter(s[1]))
  232. {
  233. int fromBase = 0;
  234. if (s[1] == 'x' || s[1] == 'X')
  235. {
  236. fromBase = 16;
  237. }
  238. if (s[1] == 'o' || s[1] == 'O')
  239. {
  240. fromBase = 8;
  241. }
  242. if (s[1] == 'b' || s[1] == 'B')
  243. {
  244. fromBase = 2;
  245. }
  246. if (fromBase > 0)
  247. {
  248. return Convert.ToInt32(s.Substring(2), fromBase);
  249. }
  250. }
  251. var start = s[0];
  252. if (start != '+' && start != '-' && start != '.' && !char.IsDigit(start))
  253. {
  254. return double.NaN;
  255. }
  256. double n = double.Parse(s,
  257. NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign |
  258. NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite |
  259. NumberStyles.AllowExponent, CultureInfo.InvariantCulture);
  260. if (s.StartsWith("-") && n == 0)
  261. {
  262. return -0.0;
  263. }
  264. return n;
  265. }
  266. catch (OverflowException)
  267. {
  268. return s.StartsWith("-") ? double.NegativeInfinity : double.PositiveInfinity;
  269. }
  270. catch
  271. {
  272. return double.NaN;
  273. }
  274. }
  275. /// <summary>
  276. /// http://www.ecma-international.org/ecma-262/#sec-tointeger
  277. /// </summary>
  278. public static double ToInteger(JsValue o)
  279. {
  280. var number = ToNumber(o);
  281. if (double.IsNaN(number))
  282. {
  283. return 0;
  284. }
  285. if (number == 0 || double.IsInfinity(number))
  286. {
  287. return number;
  288. }
  289. return (long) number;
  290. }
  291. internal static double ToInteger(string o)
  292. {
  293. var number = ToNumber(o);
  294. if (double.IsNaN(number))
  295. {
  296. return 0;
  297. }
  298. if (number == 0 || double.IsInfinity(number))
  299. {
  300. return number;
  301. }
  302. return (long) number;
  303. }
  304. /// <summary>
  305. /// http://www.ecma-international.org/ecma-262/5.1/#sec-9.5
  306. /// </summary>
  307. public static int ToInt32(JsValue o)
  308. {
  309. return o._type == InternalTypes.Integer
  310. ? o.AsInteger()
  311. : (int) (uint) ToNumber(o);
  312. }
  313. /// <summary>
  314. /// http://www.ecma-international.org/ecma-262/5.1/#sec-9.6
  315. /// </summary>
  316. public static uint ToUint32(JsValue o)
  317. {
  318. return o._type == InternalTypes.Integer
  319. ? (uint) o.AsInteger()
  320. : (uint) ToNumber(o);
  321. }
  322. /// <summary>
  323. /// http://www.ecma-international.org/ecma-262/5.1/#sec-9.7
  324. /// </summary>
  325. public static ushort ToUint16(JsValue o)
  326. {
  327. return o._type == InternalTypes.Integer
  328. ? (ushort) (uint) o.AsInteger()
  329. : (ushort) (uint) ToNumber(o);
  330. }
  331. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  332. internal static string ToString(long i)
  333. {
  334. return i >= 0 && i < intToString.Length
  335. ? intToString[i]
  336. : i.ToString();
  337. }
  338. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  339. internal static string ToString(int i)
  340. {
  341. return i >= 0 && i < intToString.Length
  342. ? intToString[i]
  343. : i.ToString();
  344. }
  345. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  346. internal static string ToString(uint i)
  347. {
  348. return i < (uint) intToString.Length
  349. ? intToString[i]
  350. : i.ToString();
  351. }
  352. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  353. internal static string ToString(char c)
  354. {
  355. return c >= 0 && c < charToString.Length
  356. ? charToString[c]
  357. : c.ToString();
  358. }
  359. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  360. internal static string ToString(ulong i)
  361. {
  362. return i >= 0 && i < (ulong) intToString.Length
  363. ? intToString[i]
  364. : i.ToString();
  365. }
  366. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  367. internal static string ToString(double d)
  368. {
  369. if (d > long.MinValue && d < long.MaxValue && Math.Abs(d % 1) <= DoubleIsIntegerTolerance)
  370. {
  371. // we are dealing with integer that can be cached
  372. return ToString((long) d);
  373. }
  374. using (var stringBuilder = StringBuilderPool.Rent())
  375. {
  376. // we can create smaller array as we know the format to be short
  377. return NumberPrototype.NumberToString(d, new DtoaBuilder(17), stringBuilder.Builder);
  378. }
  379. }
  380. /// <summary>
  381. /// http://www.ecma-international.org/ecma-262/6.0/#sec-topropertykey
  382. /// </summary>
  383. public static Key ToPropertyKey(JsValue o)
  384. {
  385. var key = ToPrimitive(o, Types.String);
  386. if (key is JsSymbol s)
  387. {
  388. return s.ToPropertyKey();
  389. }
  390. return ToString(key);
  391. }
  392. /// <summary>
  393. /// http://www.ecma-international.org/ecma-262/6.0/#sec-tostring
  394. /// </summary>
  395. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  396. public static string ToString(JsValue o)
  397. {
  398. if (o._type == InternalTypes.String)
  399. {
  400. return o.AsStringWithoutTypeCheck();
  401. }
  402. if (o._type == InternalTypes.Integer)
  403. {
  404. return ToString((int) ((JsNumber) o)._value);
  405. }
  406. return ToStringUnlikely(o);
  407. }
  408. private static string ToStringUnlikely(JsValue o)
  409. {
  410. switch (o._type)
  411. {
  412. case InternalTypes.Boolean:
  413. return ((JsBoolean) o)._value ? "true" : "false";
  414. case InternalTypes.Number:
  415. return ToString(((JsNumber) o)._value);
  416. case InternalTypes.Symbol:
  417. return ExceptionHelper.ThrowTypeErrorNoEngine<string>("Cannot convert a Symbol value to a string");
  418. case InternalTypes.Undefined:
  419. return Undefined.Text;
  420. case InternalTypes.Null:
  421. return Null.Text;
  422. case InternalTypes.Object when o is IPrimitiveInstance p:
  423. return ToString(ToPrimitive(p.PrimitiveValue, Types.String));
  424. default:
  425. return ToString(ToPrimitive(o, Types.String));
  426. }
  427. }
  428. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  429. public static ObjectInstance ToObject(Engine engine, JsValue value)
  430. {
  431. switch (value._type)
  432. {
  433. case InternalTypes.Object:
  434. return (ObjectInstance) value;
  435. case InternalTypes.Boolean:
  436. return engine.Boolean.Construct(((JsBoolean) value)._value);
  437. case InternalTypes.Number:
  438. case InternalTypes.Integer:
  439. return engine.Number.Construct(((JsNumber) value)._value);
  440. case InternalTypes.String:
  441. return engine.String.Construct(value.AsStringWithoutTypeCheck());
  442. case InternalTypes.Symbol:
  443. return engine.Symbol.Construct(((JsSymbol) value)._value);
  444. default:
  445. ExceptionHelper.ThrowTypeError(engine);
  446. return null;
  447. }
  448. }
  449. public static Types GetPrimitiveType(JsValue value)
  450. {
  451. var type = GetInternalPrimitiveType(value);
  452. return type == InternalTypes.Integer ? Types.Number : (Types) type;
  453. }
  454. internal static InternalTypes GetInternalPrimitiveType(JsValue value)
  455. {
  456. if (value._type != InternalTypes.Object)
  457. {
  458. return value._type;
  459. }
  460. if (value is IPrimitiveInstance primitive)
  461. {
  462. return (InternalTypes) primitive.Type;
  463. }
  464. return InternalTypes.Object;
  465. }
  466. internal static void CheckObjectCoercible(
  467. Engine engine,
  468. JsValue o,
  469. MemberExpression expression,
  470. string referenceName)
  471. {
  472. if (o._type < InternalTypes.Boolean && (engine.Options.ReferenceResolver?.CheckCoercible(o)).GetValueOrDefault() != true)
  473. {
  474. ThrowTypeError(engine, o, expression, referenceName);
  475. }
  476. }
  477. private static void ThrowTypeError(
  478. Engine engine,
  479. JsValue o,
  480. MemberExpression expression,
  481. string referencedName)
  482. {
  483. referencedName = referencedName ?? "The value";
  484. var message = $"{referencedName} is {o}";
  485. throw new JavaScriptException(engine.TypeError, message).SetCallstack(engine, expression.Location);
  486. }
  487. public static void CheckObjectCoercible(Engine engine, JsValue o)
  488. {
  489. if (o._type < InternalTypes.Boolean)
  490. {
  491. ExceptionHelper.ThrowTypeError(engine);
  492. }
  493. }
  494. public static IEnumerable<Tuple<MethodBase, JsValue[]>> FindBestMatch<T>(Engine engine, T[] methods, Func<T, bool, JsValue[]> argumentProvider) where T : MethodBase
  495. {
  496. List<Tuple<T, JsValue[]>> matchingByParameterCount = null;
  497. foreach (var m in methods)
  498. {
  499. bool hasParams = false;
  500. var parameterInfos = m.GetParameters();
  501. foreach (var parameter in parameterInfos)
  502. {
  503. if (Attribute.IsDefined(parameter, typeof(ParamArrayAttribute)))
  504. {
  505. hasParams = true;
  506. break;
  507. }
  508. }
  509. var arguments = argumentProvider(m, hasParams);
  510. if (parameterInfos.Length == arguments.Length)
  511. {
  512. if (methods.Length == 0 && arguments.Length == 0)
  513. {
  514. yield return new Tuple<MethodBase, JsValue[]>(m, arguments);
  515. yield break;
  516. }
  517. matchingByParameterCount = matchingByParameterCount ?? new List<Tuple<T, JsValue[]>>();
  518. matchingByParameterCount.Add(new Tuple<T, JsValue[]>(m, arguments));
  519. }
  520. else if (parameterInfos.Length > arguments.Length)
  521. {
  522. // check if we got enough default values to provide all parameters (or more in case some default values are provided/overwritten)
  523. var defaultValuesCount = 0;
  524. foreach (var param in parameterInfos)
  525. {
  526. if (param.HasDefaultValue) defaultValuesCount++;
  527. }
  528. if (parameterInfos.Length <= arguments.Length + defaultValuesCount)
  529. {
  530. // create missing arguments from default values
  531. var argsWithDefaults = new List<JsValue>(arguments);
  532. for (var i = arguments.Length; i < parameterInfos.Length; i++)
  533. {
  534. var param = parameterInfos[i];
  535. var value = JsValue.FromObject(engine, param.DefaultValue);
  536. argsWithDefaults.Add(value);
  537. }
  538. matchingByParameterCount = matchingByParameterCount ?? new List<Tuple<T, JsValue[]>>();
  539. matchingByParameterCount.Add(new Tuple<T, JsValue[]>(m, argsWithDefaults.ToArray()));
  540. }
  541. }
  542. }
  543. if (matchingByParameterCount == null)
  544. {
  545. yield break;
  546. }
  547. foreach (var tuple in matchingByParameterCount)
  548. {
  549. var perfectMatch = true;
  550. var parameters = tuple.Item1.GetParameters();
  551. var arguments = tuple.Item2;
  552. for (var i = 0; i < arguments.Length; i++)
  553. {
  554. var arg = arguments[i].ToObject();
  555. var paramType = parameters[i].ParameterType;
  556. if (arg == null)
  557. {
  558. if (!TypeIsNullable(paramType))
  559. {
  560. perfectMatch = false;
  561. break;
  562. }
  563. }
  564. else if (arg.GetType() != paramType)
  565. {
  566. perfectMatch = false;
  567. break;
  568. }
  569. }
  570. if (perfectMatch)
  571. {
  572. yield return new Tuple<MethodBase, JsValue[]>(tuple.Item1, arguments);
  573. yield break;
  574. }
  575. }
  576. for (var i = 0; i < matchingByParameterCount.Count; i++)
  577. {
  578. var tuple = matchingByParameterCount[i];
  579. yield return new Tuple<MethodBase, JsValue[]>(tuple.Item1, tuple.Item2);
  580. }
  581. }
  582. public static bool TypeIsNullable(Type type)
  583. {
  584. return !type.IsValueType || Nullable.GetUnderlyingType(type) != null;
  585. }
  586. }
  587. }