diff --git a/include/fmt/core.h b/include/fmt/core.h
index 6611d5b8..4f9115b1 100644
--- a/include/fmt/core.h
+++ b/include/fmt/core.h
@@ -306,6 +306,24 @@ template <typename T> using type_identity_t = typename type_identity<T>::type;
 template <typename T>
 using underlying_t = typename std::underlying_type<T>::type;
 
+template <typename...>
+struct disjunction : std::false_type {};
+template <typename P>
+struct disjunction<P> : P {};
+template <typename P1, typename... Pn>
+struct disjunction<P1, Pn...> 
+  : conditional_t<bool(P1::value), P1, disjunction<Pn...>> {
+};
+
+template <typename...>
+struct conjunction : std::true_type {};
+template <typename P>
+struct conjunction<P> : P {};
+template <typename P1, typename... Pn>
+struct conjunction<P1, Pn...>
+  : conditional_t<bool(P1::value), conjunction<Pn...>, P1> {
+};
+
 struct monostate {
   constexpr monostate() {}
 };
diff --git a/include/fmt/ranges.h b/include/fmt/ranges.h
index c5619922..10429fc8 100644
--- a/include/fmt/ranges.h
+++ b/include/fmt/ranges.h
@@ -246,7 +246,7 @@ template <class Tuple, class F> void for_each(Tuple&& tup, F&& f) {
   for_each(indexes, std::forward<Tuple>(tup), std::forward<F>(f));
 }
 
-#if FMT_MSC_VERSION
+#if FMT_MSC_VERSION && FMT_MSC_VERSION < 1920
 // Older MSVC doesn't get the reference type correctly for arrays.
 template <typename R> struct range_reference_type_impl {
   using type = decltype(*detail::range_begin(std::declval<R&>()));
@@ -396,15 +396,18 @@ template <typename R, typename Char>
 struct formatter<
     R, Char,
     enable_if_t<
-        fmt::is_range<R, Char>::value
-// Workaround a bug in MSVC 2019 and earlier.
-#if !FMT_MSC_VERSION
-        &&
-        (is_formattable<detail::uncvref_type<detail::maybe_const_range<R>>,
-                        Char>::value ||
-         detail::has_fallback_formatter<
-             detail::uncvref_type<detail::maybe_const_range<R>>, Char>::value)
+        conjunction<fmt::is_range<R, Char>
+// Workaround a bug in MSVC 2017 and earlier.
+#if !FMT_MSC_VERSION || FMT_MSC_VERSION >= 1920
+        ,
+        disjunction<
+          is_formattable<detail::uncvref_type<detail::maybe_const_range<R>>,
+                         Char>,
+          detail::has_fallback_formatter<
+             detail::uncvref_type<detail::maybe_const_range<R>>, Char>
+        >
 #endif
+        >::value
         >> {
 
   using range_type = detail::maybe_const_range<R>;
@@ -457,14 +460,20 @@ struct formatter<
 template <typename T, typename Char>
 struct formatter<
     T, Char,
-    enable_if_t<detail::is_map<T>::value
-// Workaround a bug in MSVC 2019 and earlier.
-#if !FMT_MSC_VERSION
-        && (is_formattable<detail::uncvref_first_type<T>, Char>::value ||
-            detail::has_fallback_formatter<detail::uncvref_first_type<T>, Char>::value)
-        && (is_formattable<detail::uncvref_second_type<T>, Char>::value ||
-            detail::has_fallback_formatter<detail::uncvref_second_type<T>, Char>::value)
+    enable_if_t<conjunction<detail::is_map<T>
+// Workaround a bug in MSVC 2017 and earlier.
+#if !FMT_MSC_VERSION || FMT_MSC_VERSION >= 1920
+        ,
+        disjunction<
+          is_formattable<detail::uncvref_first_type<T>, Char>,
+          detail::has_fallback_formatter<detail::uncvref_first_type<T>, Char>
+        >,
+        disjunction<
+          is_formattable<detail::uncvref_second_type<T>, Char>,
+          detail::has_fallback_formatter<detail::uncvref_second_type<T>, Char>
+        >
 #endif
+    >::value
     >> {
   template <typename ParseContext>
   FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
diff --git a/include/fmt/std.h b/include/fmt/std.h
index 41d2b283..227f4841 100644
--- a/include/fmt/std.h
+++ b/include/fmt/std.h
@@ -57,6 +57,10 @@ inline void write_escaped_path<std::filesystem::path::value_type>(
 
 }  // namespace detail
 
+#if !FMT_MSC_VERSION || FMT_MSC_VERSION >= 1920
+// For MSVC 2017 and earlier using the partial specialization
+// would cause an ambiguity error, therefore we provide it only
+// conditionally.
 template <typename Char>
 struct formatter<std::filesystem::path, Char>
     : formatter<basic_string_view<Char>> {
@@ -69,6 +73,7 @@ struct formatter<std::filesystem::path, Char>
         basic_string_view<Char>(quoted.data(), quoted.size()), ctx);
   }
 };
+#endif
 FMT_END_NAMESPACE
 #endif
 
diff --git a/test/std-test.cc b/test/std-test.cc
index 02b5a591..fc8f72a0 100644
--- a/test/std-test.cc
+++ b/test/std-test.cc
@@ -6,13 +6,18 @@
 // For the license information refer to format.h.
 
 #include "fmt/std.h"
+#include "fmt/ranges.h"
 
 #include <string>
+#include <vector>
 
 #include "gtest/gtest.h"
 
 TEST(std_test, path) {
-#ifdef __cpp_lib_filesystem
+// Test ambiguity problem described in #2954. We need to exclude compilers
+// where the ambiguity problem cannot be solved for now.
+#if defined(__cpp_lib_filesystem) && \
+    (!FMT_MSC_VERSION || FMT_MSC_VERSION >= 1920)
   EXPECT_EQ(fmt::format("{:8}", std::filesystem::path("foo")), "\"foo\"   ");
   EXPECT_EQ(fmt::format("{}", std::filesystem::path("foo\"bar.txt")),
             "\"foo\\\"bar.txt\"");
@@ -31,6 +36,18 @@ TEST(std_test, path) {
 #endif
 }
 
+TEST(ranges_std_test, format_vector_path) {
+// Test ambiguity problem described in #2954. We need to exclude compilers
+// where the ambiguity problem cannot be solved for now.
+#if defined(__cpp_lib_filesystem) && \
+    (!FMT_MSC_VERSION || FMT_MSC_VERSION >= 1920)
+  auto p = std::filesystem::path("foo/bar.txt");
+  auto c = std::vector<std::string>{"abc", "def"};
+  EXPECT_EQ(fmt::format("path={}, range={}", p, c),
+            "path=\"foo/bar.txt\", range=[\"abc\", \"def\"]");
+#endif
+}
+
 TEST(std_test, thread_id) {
   EXPECT_FALSE(fmt::format("{}", std::this_thread::get_id()).empty());
 }